摘要:使用框架各个组件实现一个在线聊天网页,当有用户连接,服务器监听到用户连接会使用推送最新用户列表,有用户断开刷新在线列表,实时推送用户聊天信息。根据请求头是否等于判断是否是。
使用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查看
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/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...
阅读 3556·2021-08-02 13:41
阅读 2388·2019-08-30 15:56
阅读 1518·2019-08-30 11:17
阅读 1173·2019-08-29 15:18
阅读 578·2019-08-29 11:10
阅读 2669·2019-08-26 13:52
阅读 506·2019-08-26 13:22
阅读 2947·2019-08-23 15:41