摘要:使用框架各個組件實現一個在線聊天網頁,當有用戶連接,服務器監聽到用戶連接會使用推送最新用戶列表,有用戶斷開刷新在線列表,實時推送用戶聊天信息。根據請求頭是否等于判斷是否是。
使用Spring框架各個組件實現一個在線聊天網頁,當有用戶連接WebSocket,服務器監聽到用戶連接會使用Stomp推送最新用戶列表,有用戶斷開刷新在線列表,實時推送用戶聊天信息。引入Jetty服務器,直接嵌入整個工程可以脫離Java Web容器獨立運行,使用插件打包成一個jar文件,就像Spring Boot一樣運行,部署。
pom.xml 依賴
1. 配置H2 嵌入式數據庫UTF-8 UTF-8 UTF-8 9.4.8.v20171121 5.0.4.RELEASE 2.9.4 1.16.18 1.4.196 1.7.25 5.0.3.RELEASE 1.2.3 5.15.0 org.eclipse.jetty jetty-servlet ${jetty.version} org.eclipse.jetty.websocket websocket-server ${jetty.version} org.eclipse.jetty.websocket websocket-api ${jetty.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-jdbc ${spring.version} org.springframework spring-webflux ${spring.version} org.springframework spring-websocket ${spring.version} org.springframework spring-messaging ${spring.version} org.springframework.security spring-security-web ${spring.security.version} org.springframework.security spring-security-config ${spring.security.version} org.springframework.security spring-security-oauth2-client ${spring.security.version} org.springframework.security spring-security-oauth2-jose ${spring.security.version} org.springframework.security spring-security-messaging ${spring.security.version} com.fasterxml.jackson.core jackson-core ${jackson.version} com.h2database h2 ${dbh2.version} com.fasterxml.jackson.core jackson-databind ${jackson.version} org.projectlombok lombok ${lombok.version} org.slf4j jcl-over-slf4j ${jcl.slf4j.version} ch.qos.logback logback-classic ${logback.version} org.apache.activemq activemq-broker ${activemq.version} io.projectreactor.ipc reactor-netty 0.7.2.RELEASE
@Bean //內存模式 public DataSource dataSource(){ EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); EmbeddedDatabase build = builder.setType(EmbeddedDatabaseType.H2) .addScript("db/sql/create-db.sql") //每次創建數據源都會執行腳本 .addScript("db/sql/insert-data.sql") .build(); return build; }
這種方式是利用Spring 內置的嵌入式數據庫的數據源模板,創建的數據源,比較簡單,但是這種方式不支持定制,數據只能保存在內存中,項目重啟數據就會丟失了。
設置數據保存到硬盤
@Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUsername("embedded"); dataSource.setPassword("embedded"); dataSource.setUrl("jdbc:h2:file:./data;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;"); return dataSource; }
如果你還想每次創建數據源執行初始化sql,使用org.springframework.jdbc.datasource.init.ResourceDatabasePopulator 裝載sql 腳本用于初始化或清理數據庫
@Bean public ResourceDatabasePopulator databasePopulator() { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.addScript(schema); populator.addScripts(data); populator.setContinueOnError(true); return populator; }
設置DatabasePopulator 對象,用戶數據源啟動或者消耗的時候執行腳本
@Bean public DataSourceInitializer initializer() { DataSourceInitializer initializer = new DataSourceInitializer(); initializer.setDatabasePopulator(databasePopulator()); initializer.setDataSource(dataSource()); return initializer; }
啟用H2 web Console
@Bean(initMethod = "start",destroyMethod = "stop") public Server DatasourcesManager() throws SQLException { return Server.createWebServer("-web","-webAllowOthers","-webPort","8082"); }
瀏覽器打開 http://localhost:8082 訪問H2 控制臺
設置事務管理器
@Bean public PlatformTransactionManager transactionManager() { PlatformTransactionManager manager = new DataSourceTransactionManager(dataSource()); return manager; } }
到這里,嵌入H2數據庫配置基本已經設置完成了
2. Spring MVC配置import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @Configuration @EnableWebMvc @ComponentScan(basePackages = "org.ting.spring.controller", //基包路徑設置 includeFilters = @ComponentScan.Filter(value = {ControllerAdvice.class,Controller.class})) //只掃描MVC controll的注解 public class WebMvcConfiguration implements WebMvcConfigurer { public void extendMessageConverters(List3. Jetty嵌入式服務> converters) { converters.add(new MappingJackson2HttpMessageConverter()); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //添加靜態路徑映射 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
因為Spring 注解掃描只能注冊一個類, 使用@Import引入其他的配置類
@Configuration @ComponentScan(basePackages = "org.ting.spring", excludeFilters = {@ComponentScan.Filter(value = {Controller.class,ControllerAdvice.class})}) @Import({WebMvcConfiguration.class}) //引入Spring MVC配置類 public class WebRootConfiguration { @Autowired private DataSource dataSource; @Bean public JdbcTemplate jdbcTemplate(){ JdbcTemplate template = new JdbcTemplate(dataSource); return template; } }
使用Spring AnnotationConfigWebApplicationContext 啟動注解掃描,注冊創建bean將WebApplicationContext,在將對象傳給DispatcherServlet
public class JettyEmbedServer { private final static int DEFAULT_PORT = 9999; private final static String DEFAULT_CONTEXT_PATH = "/"; private final static String MAPPING_URL = "/*"; public static void main(String[] args) throws Exception { Server server = new Server(DEFAULT_PORT); JettyEmbedServer helloServer = new JettyEmbedServer(); server.setHandler(helloServer.servletContextHandler()); server.start(); server.join(); } private ServletContextHandler servletContextHandler() { WebApplicationContext context = webApplicationContext(); ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); servletContextHandler.setContextPath(DEFAULT_CONTEXT_PATH); ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(context)); servletHolder.setAsyncSupported(true); servletContextHandler.addServlet(servletHolder, MAPPING_URL); return servletContextHandler; } private WebApplicationContext webApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(WebRootConfiguration.class); return context; }3. 配置Spring Security
默認Spring Security攔截請求,登錄失敗,登錄成功都是頁面跳轉的方式,我們希望ajax請求的時候,無論是被攔截了,或者登錄失敗,成功都可以返回json格式數據,由前端人員來處理。
根據HttpRequestServlet 請求頭 X-Requested-With是否等于XMLHttpRequest 判斷是否是ajax。
public class RespnonseJson { public static void jsonType(HttpServletResponse response) { response.setContentType("application/json;charset=UTF-8"); response.setCharacterEncoding("utf-8"); } public static boolean ajaxRequest(HttpServletRequest request){ String header = request.getHeader("X-Requested-With"); return ! StringUtils.isEmpty(header) && header.equals("XMLHttpRequest"); } public static boolean matchURL(String url) { Pattern compile = Pattern.compile("^/api/.+"); return compile.matcher(url).matches(); } }
登錄認證處理器
public class RestAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { /** * @param loginFormUrl URL where the login page can be found. Should either be * relative to the web-app context path (include a leading {@code /}) or an absolute * URL. */ public RestAuthenticationEntryPoint(String loginFormUrl) { super(loginFormUrl); } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String uri = request.getRequestURI(); if (matchURL(uri)) { // /api 都是ajax 請求 jsonType(response); response.getWriter().println(getErr(authException.getMessage())); }else if (ajaxRequest(request)){ jsonType(response); response.getWriter().println(getErr(authException.getMessage())); }else super.commence(request,response,authException); } private String getErr(String description) throws JsonProcessingException { Result result = Result.error(Result.HTTP_FORBIDDEN, description); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(result); } }
登錄成功處理
public class RestAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String uri = request.getRequestURI(); if (matchURL(uri)){ jsonType(response); String value = loginSuccess(); response.getWriter().println(value); }else if (ajaxRequest(request)){ jsonType(response); String success = loginSuccess(); response.getWriter().println(success); }else super.onAuthenticationSuccess(request,response,authentication); } private String loginSuccess() throws JsonProcessingException { Result success = Result.success("sign on success go to next!"); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(success); } }
登錄失敗處理
public class RestAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (ajaxRequest(request)){ jsonType(response); String err = getErr(exception.getMessage()); response.getWriter().println(err); }else super.onAuthenticationFailure(request,response,exception); } public String getErr(String description) throws JsonProcessingException { Result result = Result.error(Result.HTTP_AUTH_FAILURE, description); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(result); } }
我在網上搜索ajax 認證錯誤,很多博客是這樣寫的
response.sendError(500, "Authentication failed");
這個錯誤會被Jetty 錯誤頁面捕獲,擾亂返回JSON數據,這個細節要注意下
注冊Handler
@Bean public AuthenticationEntryPoint entryPoint() { RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint("/static/html/login.html"); return entryPoint; } @Bean public SimpleUrlAuthenticationSuccessHandler successHandler() { RestAuthSuccessHandler successHandler = new RestAuthSuccessHandler(); return successHandler; } @Bean public SimpleUrlAuthenticationFailureHandler failureHandler() { RestAuthFailureHandler failureHandler = new RestAuthFailureHandler(); return failureHandler; }
配置url 認證
@Bean public SessionRegistry sessionManager() { return new SessionRegistryImpl(); } @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(entryPoint()) .and() .authorizeRequests() .antMatchers("/static/html/jetty-chat.html", "/api/user/online", "/api/user/loginuser") .authenticated() //設置需要認證才可以請求的接口 .and() .formLogin() .successHandler(successHandler()) //登錄成功處理 .failureHandler(failureHandler()) //登錄失敗處理 .loginPage("/static/html/login.html") //登錄頁面 .loginProcessingUrl("/auth/login") //登錄表單url .defaultSuccessUrl("/static/html/jetty-chat.html") //成功跳轉url .permitAll() .and().csrf().disable()//禁用csrf 因為沒有使用模板引擎 .sessionManagement().maximumSessions(1) //設置同一個賬戶,同時在線次數 .sessionRegistry(sessionManager()) // 設置Session 管理器, .expiredUrl("/static/html/login.html") //session 失效后,跳轉url .maxSessionsPreventsLogin(false) //設置true,達到session 最大登錄次數后,后面的賬戶都會登錄失敗,false 頂號 前面登錄賬戶會被后面頂下線 ; //注銷賬戶,跳轉到登錄頁面 http.logout().logoutUrl("/logout").logoutSuccessUrl("/static/html/login.html");
在配置類添加@EnableWebSecurity,在掃描類上引入Spring Security配置,大功告成了,并沒有!Spring Security 是使用Filter來處理一些認證請求,需要我們在Jetty中手動注冊攔截器
//手動注冊攔截器,讓Spring Security 生效 FilterHolder filterHolder = new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain")); servletContextHandler.addFilter(filterHolder, MAPPING_URL, null); servletContextHandler.addEventListener(new ContextLoaderListener(context)); servletContextHandler.addEventListener(new HttpSessionEventPublisher()); //使用security session 監聽器 限制只允許一個用戶登錄4. 配置WebSocketStompConfig
@Configuration @EnableWebSocketMessageBroker @ComponentScan(basePackages = "org.ting.spring.stomp.message") @Slf4j public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { //設置連接的端點路徑 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("endpoint").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 定義了兩個客戶端訂閱地址的前綴信息,也就是客戶端接收服務端發送消息的前綴信息 registry.enableSimpleBroker("/topic", "/queue"); // 定義了服務端接收地址的前綴,也即客戶端給服務端發消息的地址前綴 registry.setApplicationDestinationPrefixes("/app"); //使用客戶端一對一通信 registry.setUserDestinationPrefix("/user"); registry.setPathMatcher(new AntPathMatcher(".")); } }
配置stomp 頻道認證
@Configuration public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages.simpDestMatchers("/user/**").authenticated()//認證所有user 鏈接 .anyMessage().permitAll(); } //允許跨域 不然會出現 Could not verify the provided CSRF token because your session was not found 異常 @Override protected boolean sameOriginDisabled() { return true; } }
信息處理
@Controller @Slf4j public class StompController { @Autowired private SimpMessagingTemplate messagingTemplate; @MessageExceptionHandler @SendToUser("/queue.errors") public String handleException(Throwable exception) { return exception.getMessage(); } @MessageMapping("receive.messgae") public void forwardMsg(ChatMessage message){ log.info("message : {}",message); message.setLocalDateTime(LocalDateTime.now()); messagingTemplate.convertAndSendToUser(message.getTargetUser().getEmail() ,"queue.notification",message); } }
@MessageMapping 作用與@RequestMapping 功能差不多用于匹配url
更多Spring WebSocket 官方文檔查看
我們使用一個集合來保存連接上的用戶,使用連接,斷開監聽器來修改集合的列表,并將集合的數據發布到頻道上。
websocket 斷開連接監聽器
@Component @Slf4j public class WebSocketDisconnectListener implements ApplicationListener{ @Autowired private UserService userService; @Autowired private SimpMessagingTemplate messageTemplate; @Override public void onApplicationEvent(SessionDisconnectEvent event) { Principal principal = event.getUser(); log.info("client sessionId : {} name : {} disconnect ....",event.getSessionId(),principal.getName()); if (principal != null){ //已經認證過的用戶 User user = userService.findByEmail(principal.getName()); Online.remove(user); messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers()); } } }
注冊連接websocket 監聽器
@Component @Slf4j public class WebSocketSessionConnectEvent implements ApplicationListener{ @Autowired private SimpMessagingTemplate messageTemplate; @Autowired private UserService userService; @Override public void onApplicationEvent(SessionConnectEvent event) { Principal principal = event.getUser(); log.info("client name: {} connect.....",principal.getName()); if (principal != null){ User user = userService.findByEmail(principal.getName()); Online.add(user); messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers()); } } }
保存在線列表
public class Online { private static Map4. Spring Security OAuth2 Client 配置maps = new ConcurrentHashMap<>(); public static void add(User user){ maps.put(user.getEmail(),user); } public static void remove(User user){ maps.remove(user.getEmail()); } public static Collection onlineUsers(){ return maps.values(); } }
手動配置ClientRegistrationRepository 設置client-id,client-secret,redirect-uri-template
@Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(githubClientRegstrationRepository() ,googleClientRegistrionRepository()); } public ClientRegistration githubClientRegstrationRepository(){ return CommonOAuth2Provider.GITHUB.getBuilder("github") .clientId(env.getProperty("registration.github.client-id")) .clientSecret(env.getProperty("registration.github.client-secret")) .redirectUriTemplate(env.getProperty("registration.github.redirect-uri-template")) .build(); } public ClientRegistration googleClientRegistrionRepository(){ return CommonOAuth2Provider.GOOGLE.getBuilder("google") .clientId(env.getProperty("registration.google.client-id")) .clientSecret(env.getProperty("registration.google.client-secret")) .redirectUriTemplate(env.getProperty("registration.google.redirect-uri-template")) .scope( "profile", "email") .build(); } @Bean public OAuth2AuthorizedClientService authorizedClientService() { return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository()); }
我們使用github,google OAuth2 授權登錄的賬戶,登錄通過后保存起來,則需求繼承DefaultOAuth2UserService
@Service @Slf4j public class CustomOAuth2UserService extends DefaultOAuth2UserService { @Autowired private UserService userService; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User = super.loadUser(userRequest); try { oAuth2User = processOAuth2User(oAuth2User,userRequest); } catch (Exception e) { log.error("processOAuth2User error {}",e); } return oAuth2User; } private OAuth2User processOAuth2User(OAuth2User oAuth2User,OAuth2UserRequest userRequest) { String clientId = userRequest.getClientRegistration().getRegistrationId(); if (clientId.equalsIgnoreCase("github")) { Mapmap = oAuth2User.getAttributes(); String login = map.get("login")+"_oauth_github"; String name = (String) map.get("name"); String avatarUrl = (String) map.get("avatar_url"); User user = userService.findByEmail(login); if (user == null) { user = new User(); user.setUsername(name); user.setEmail(login); user.setAvatar(avatarUrl); user.setPassword("123456"); userService.insert(user); }else { user.setUsername(name); user.setAvatar(avatarUrl); userService.update(user); } return UserPrincipal.create(user, oAuth2User.getAttributes()); }else if (clientId.equalsIgnoreCase("google")){ Map result = oAuth2User.getAttributes(); String email = result.get("email")+"_oauth_google"; String username = (String) result.get("name"); String imgUrl = (String) result.get("picture"); User user = userService.findByEmail(email); if (user == null){ user = new User(); user.setEmail(email); user.setPassword("123456"); user.setAvatar(imgUrl); user.setUsername(username); userService.insert(user); }else { user.setUsername(username); user.setAvatar(imgUrl); userService.update(user); } return UserPrincipal.create(user,oAuth2User.getAttributes()); } return null; } }
重寫UserDetails
public class UserPrincipal implements OAuth2User,UserDetails { private long id; private String name; private String password; private boolean enable; private Collection extends GrantedAuthority> authorities; private Mapattributes; UserPrincipal(long id,String name,String password,boolean enable,Collection extends GrantedAuthority> authorities){ this.id = id; this.name = name; this.password = password; this.authorities = authorities; this.enable = enable; } public static UserPrincipal create(User user){ return new UserPrincipal(user.getId(),user.getEmail() ,user.getPassword(),user.isEnable(),Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); } public static UserPrincipal create(User user, Map attributes) { UserPrincipal userPrincipal = UserPrincipal.create(user); userPrincipal.attributes = attributes; return userPrincipal; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return name; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enable; } @Override public Collection extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public Map getAttributes() { return this.attributes; } @Override public String getName() { return String.valueOf(this.id); } }
設置Spring Security OAuth2 Client
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomOAuth2UserService customOAuth2UserService; @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Login() .clientRegistrationRepository(clientRegistrationRepository()) .authorizedClientService(authorizedClientService()) .userInfoEndpoint() .userService(customOAuth2UserService) .and() .defaultSuccessUrl("/static/html/jetty-chat.html"); } }
默認授權端點,點擊后直接重定向到授權服務器的登錄頁面,Spring 默認是: oauth2/authorization/{clientId}
默認授權成功跳轉url: /login/oauth2/code/{clientId}
這個項目參考的教程:
https://www.baeldung.com/spring-security-5-oauth2-login
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/
這個教程只展示了一部分的代碼,想查看完整的項目代碼,可以去github: spring-stomp-security-webflux-embedded-jetty查看
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73414.html
摘要:在逐步開發過程中,發現自己需求,用戶使用,頁面樣式,做得都不是很好。希望很和牛逼的人合作,一齊完善這個項目,能讓它變成可以使用的產品。自己也可以在此不斷學習,不斷累計新的知識,慢慢變強起來。 showImg(https://segmentfault.com/img/bVboKz5);#### 這一個什么項目 ##### 使用技術 Spring MVC Spring Security ...
摘要:子協議只是一個消息傳遞的體系結構,沒有指定任何的消息傳遞協議。是一個簡單的消息傳遞協議,是一種為,面向消息的中間件設計的簡單文本協議。的實現對內嵌的或者和使用了提供了支持。廣播式廣播式即服務端有消息時,會將消息發送到所有連接了當前的瀏覽器。 簡單介紹 WebSocket是為瀏覽器和服務端提供雙工藝部通信功能一種工具,即瀏覽器可以先服務端發送消息,服務端也可以先瀏覽器發送消息。現...
摘要:子協議只是一個消息傳遞的體系結構,沒有指定任何的消息傳遞協議。是一個簡單的消息傳遞協議,是一種為,面向消息的中間件設計的簡單文本協議。的實現對內嵌的或者和使用了提供了支持。廣播式廣播式即服務端有消息時,會將消息發送到所有連接了當前的瀏覽器。 簡單介紹 WebSocket是為瀏覽器和服務端提供雙工藝部通信功能一種工具,即瀏覽器可以先服務端發送消息,服務端也可以先瀏覽器發送消息。現...
摘要:最后發現每一個在連接端點之前都會發送一個請求用于保證該服務是存在的。而該請求是程序自動發送的自能自動攜帶數據,無法發送自定義。問題是我們的是使用自定義實現的認證。所以該方法不成立,所以只能讓自己認證。 使用框架介紹 spring boot 1.4.3.RELEASE spring websocket 4.3.5.RELEASE spring security 4.1.3.RELEAS...
閱讀 3557·2021-08-02 13:41
閱讀 2391·2019-08-30 15:56
閱讀 1520·2019-08-30 11:17
閱讀 1174·2019-08-29 15:18
閱讀 580·2019-08-29 11:10
閱讀 2671·2019-08-26 13:52
閱讀 508·2019-08-26 13:22
閱讀 2949·2019-08-23 15:41