websocket+springboot+springsecurity+springsession结合使用

    xiaoxiao2022-07-14  246

    此篇是在上一篇《websocket简介及结合springboot使用》基础上增加了springsecurity与springsession框架。使用这两个框架进行session与用户权限的管理。

    目录

    一、与springsession结合二、与springsecurity结合三、增加监听器监听用户连接时监听用户断开连接时 四、增加security配置五、security与session结合六、前端实现七、controller代码方法1方法2方法3 总结

    一、与springsession结合

    修改原先的websocketconfig文件,与springsession结合使用后,实现类也发生了改变。

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.session.Session; import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableScheduling @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<Session> { // <1> @Autowired private MyHandShakeInterceptor myHandShakeInterceptor; @Autowired private MyChannelInterceptorAdapter myChannelInterceptorAdapter; @Override protected void configureStompEndpoints(StompEndpointRegistry registry) { // <2> //注意 下面的这个url需要在springsecurity中配置允许访问,否则会被重定向,最后websocket报错302 registry.addEndpoint("/port") //添加STOMP协议的端点。这个HTTP URL是供WebSocket或SockJS客户端访问的地址 .setAllowedOrigins("*") // 添加允许跨域访问 .addInterceptors(myHandShakeInterceptor) // 添加自定义拦截 .withSockJS() //如果前台使用sockJs,此处没有设置,websocket报错404 .setClientLibraryUrl( "https://cdn.jsdelivr.net/npm/sockjs-client@1.3.0/dist/sockjs.min.js" ); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //推送消息前缀,消息的发送的地址符合配置的前缀来的消息才发送到这个broker registry.enableSimpleBroker("/queue", "/topic"); //客户端给服务端发消息的地址的前缀 registry.setApplicationDestinationPrefixes("/app"); //推送用户前缀 registry.setUserDestinationPrefix("/user"); } }

    二、与springsecurity结合

    通过security对消息进行安全设置

    import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer; @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { // @formatter:off @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages.nullDestMatcher().authenticated() //任何没有目的地的消息(即消息类型为MESSAGE或SUBSCRIBE以外的任何消息)将要求用户进行身份验证 .simpSubscribeDestMatchers("/user/queue/errors").permitAll() //任何人都可以订阅/ user / queue / error .simpDestMatchers("/app/**").hasRole("USER") //任何目的地以“/ app /”开头的消息都要求用户具有角色ROLE_USER .anyMessage().denyAll(); //拒绝任何其他消息。这是一个好主意,以确保您不会错过任何消息。 } // @formatter:on @Override protected boolean sameOriginDisabled() { return true; } }

    三、增加监听器

    为了更好的了解用户登录的日志情况,当用户连接和断开连接时候需要进行日志记录,这里使用监听器实现。

    监听用户连接时

    import org.springframework.context.ApplicationListener; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionConnectEvent; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class WebsocketConnectListener implements ApplicationListener<SessionConnectEvent>{ @Override public void onApplicationEvent(SessionConnectEvent event) { final StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(event.getMessage()); String sessionId = stompHeaderAccessor.getSessionId(); log.info("sessionId: {} 连接",sessionId); } }

    监听用户断开连接时

    import org.springframework.context.ApplicationListener; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionDisconnectEvent; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> { @Override public void onApplicationEvent(SessionDisconnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); //获取SessionId String sessionId = sha.getSessionId(); log.info("sessionId: {} 断开连接",sessionId); } }

    四、增加security配置

    import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll(); http.headers().frameOptions().disable(); http.csrf().disable(); } }

    在application.yml中增加security默认用户,即相当于在内存中创建一个用户:

    spring: security: user: name: admin password: admin

    五、security与session结合

    import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; @Configuration @EnableRedisHttpSession public class SessionConfig { @Bean public JedisConnectionFactory connectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setHostName("116.**.**.194"); redisStandaloneConfiguration.setDatabase(3); redisStandaloneConfiguration.setPassword(RedisPassword.of("***")); redisStandaloneConfiguration.setPort(6479); return new JedisConnectionFactory(redisStandaloneConfiguration); } } import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import sun.security.krb5.Config; public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityInitializer() { super(SessionConfig.class, Config.class); } }

    六、前端实现

    <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>websocket测试页面2-发送给指定的人</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> <script src="stomp.min.js"></script> <!-- <script src="sockjs.min.js"></script> --> <script src="sockjs.js"></script> </head> <body> <h2>websocket测试页面2-发送给指定的人</h2> <div> <div> <div> <button id="connect" onclick="connect();">连接</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button> </div> <div id="conversationDiv"> <label>输入你的名字</label><input type="text" id="name" /> <button id="sendName" onclick="sendName();">发送</button> <p id="response"></p> </div> </div> </body> <script type="text/javascript"> var stompClient = null; function setConnected(connected) { document.getElementById('connect').disabled = connected; document.getElementById('disconnect').disabled = !connected; document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden'; $('#response').html(); } function connect() { // websocket的连接地址,此值等于WebSocketMessageBrokerConfigurer中registry.addEndpoint("/websocket-simple").withSockJS()配置的地址 //使用此种方式,在登陆后才可以实现websocket服务端发送信息给制定用户 var socket = new SockJS('http://localhost:6543/port'); stompClient = Stomp.over(socket); //stompClient = Stomp.client("ws://127.0.0.1:6543/port"); stompClient.connect({}, function(frame) { setConnected(true); console.log('Connected: ' + frame); // 客户端订阅消息的目的地址:此值BroadcastCtl中被@SendTo("/topic/getResponse")注解的里配置的值 ///user/zhang/queue/getResponse stompClient.subscribe('/user/topic/getResponse', function(respnose){ showResponse(JSON.parse(respnose.body).responseMessage); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { var name = $('#name').val(); // 客户端消息发送的目的:服务端使用BroadcastCtl中@MessageMapping("/receive")注解的方法来处理发送过来的消息 stompClient.send("/app/receive", {}, JSON.stringify({ 'name': name })); } function showResponse(message) { var response = $("#response"); response.html(message + "\r\n" + response.html()); } </script> </html>

    七、controller代码

    package com.sample.demo.controller; import java.security.Principal; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SendToUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.sample.demo.entity.RequestMessage; import com.sample.demo.entity.ResponseMessage; @Controller public class SockerController { @Autowired private SimpMessagingTemplate simpMessagingTemplate; // 收到消息记数 private AtomicInteger count = new AtomicInteger(0); /** * 作用: 通过user发送给指定人 <br> */ @MessageMapping("/receive") // @SendTo("/topic/getResponse") // @SendToUser("/topic/getResponse") public void broadcast(RequestMessage requestMessage,Principal principal){ simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/topic/getResponse", "{\"test\":\"aaa\"}"); System.out.println("接收:===="+requestMessage.getName()+"发送:====="+principal.getName()); } /** * 作用: 通过sessionId发送给指定人 <br> */ @MessageMapping("/receive") // @SendTo("/topic/getResponse") // @SendToUser("/topic/getResponse") public void broadcast(RequestMessage requestMessage,SimpMessageHeaderAccessor headerAccessor){ String sessionId = headerAccessor.getSessionId(); MessageHeaders createHeaders = createHeaders(sessionId); simpMessagingTemplate.convertAndSendToUser(sessionId, "/topic/getResponse", "{\"test\":\"aaa\"}",createHeaders); System.out.println("接收:===="+requestMessage.getName()+"发送:====="+sessionId); } /** * 作用: 通过注解发送给指定人 <br> */ @MessageMapping("/receive") @SendTo("/topic/getResponse") @SendToUser("/topic/getResponse") public ResponseMessage broadcastMulti(RequestMessage requestMessage){ System.out.println("点对点发送"); ResponseMessage responseMessage = new ResponseMessage(); responseMessage.setResponseMessage("BroadcastCtl receive [" + count.incrementAndGet() + "] records"); return responseMessage; } @RequestMapping(value="/websocket-single") public String broadcastIndex(){ return "websocket-single"; } private MessageHeaders createHeaders(String sessionId){ final SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); headerAccessor.setSessionId(sessionId); //是否为基于多个进行信息发送 headerAccessor.setLeaveMutable(true); return headerAccessor.getMessageHeaders(); } }

    这里使用了三种发送给前端的方式:

    方法1

    第一种是通过用户名发送,此种方式需要使用登录页面,使用springsecurity指定登录页面及登录成功后的页面,登录完毕后再使用websocket发送数据到controller时候就会携带用户信息。 如果没有登录就发送数据的话,会报一个没有user的错误。

    下面是登录页面及security配置代码:

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8" /> <head> <title>登陆页面</title> </head> <body> <div th:if="${param.error}"> 无效的账号和密码 </div> <div th:if="${param.logout}"> 你已注销 </div> <form th:action="@{/login}" method="post"> <div><label> 账号 : <input type="text" name="username"/> </label></div> <div><label> 密码: <input type="password" name="password"/> </label></div> <div><input type="submit" value="登陆"/></div> </form> </body> </html> http .authorizeRequests() .antMatchers("/","/login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/websocket-single") .permitAll() .and() .logout() .permitAll();

    方法2

    第二种方法是通过sessionId发送给指定的用户,使用这种方式需要手动设置一下用户的header,在这里是调用一下controller中的createHeaders。

    前面两种方式是可以实现异步处理websocket的请求,处理完毕后可以通过user或者sessionId发送给当初请求的用户。

    方法3

    第三种方法是通过注解处理是同步的操作,但是也是最简单的方式。

    总结

    可以根据自己的需要进行选择配置方式。

    最新回复(0)