此篇是在上一篇《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> {
@Autowired
private MyHandShakeInterceptor myHandShakeInterceptor
;
@Autowired
private MyChannelInterceptorAdapter myChannelInterceptorAdapter
;
@Override
protected void configureStompEndpoints(StompEndpointRegistry registry
) {
registry
.addEndpoint("/port")
.setAllowedOrigins("*")
.addInterceptors(myHandShakeInterceptor
)
.withSockJS()
.setClientLibraryUrl( "https://cdn.jsdelivr.net/npm/sockjs-client@1.3.0/dist/sockjs.min.js" );
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry
) {
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 {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages
) {
messages
.nullDestMatcher().authenticated()
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/app/**").hasRole("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());
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.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() {
var socket = new SockJS('http://localhost:6543/port');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
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();
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);
@MessageMapping("/receive")
public void broadcast(RequestMessage requestMessage
,Principal principal
){
simpMessagingTemplate
.convertAndSendToUser(principal
.getName(), "/topic/getResponse", "{\"test\":\"aaa\"}");
System
.out
.println("接收:===="+requestMessage
.getName()+"发送:====="+principal
.getName());
}
@MessageMapping("/receive")
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
);
}
@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
第三种方法是通过注解处理是同步的操作,但是也是最简单的方式。
总结
可以根据自己的需要进行选择配置方式。