此篇是在上一篇《websocket简介及结合springboot使用》基础上增加了springsecurity与springsession框架。使用这两个框架进行session与用户权限的管理。
修改原先的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/[email protected]/dist/sockjs.min.js" );
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//推送消息前缀,消息的发送的地址符合配置的前缀来的消息才发送到这个broker
registry.enableSimpleBroker("/queue", "/topic");
//客户端给服务端发消息的地址的前缀
registry.setApplicationDestinationPrefixes("/app");
//推送用户前缀
registry.setUserDestinationPrefix("/user");
}
}
通过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);
}
}
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
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.62.16.194");
redisStandaloneConfiguration.setDatabase(3);
redisStandaloneConfiguration.setPassword(RedisPassword.of("mas@2018_redis"));
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);
}
}
<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() {
// 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>
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发送给指定人
*/
@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发送给指定人
*/
@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);
}
/**
* 作用: 通过注解发送给指定人
*/
@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();
}
}
这里使用了三种发送给前端的方式:
第一种是通过用户名发送,此种方式需要使用登录页面,使用springsecurity指定登录页面及登录成功后的页面,登录完毕后再使用websocket发送数据到controller时候就会携带用户信息。
如果没有登录就发送数据的话,会报一个没有user的错误。
下面是登录页面及security配置代码:
<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();
第二种方法是通过sessionId发送给指定的用户,使用这种方式需要手动设置一下用户的header,在这里是调用一下controller中的createHeaders。
前面两种方式是可以实现异步处理websocket的请求,处理完毕后可以通过user或者sessionId发送给当初请求的用户。
第三种方法是通过注解处理是同步的操作,但是也是最简单的方式。
可以根据自己的需要进行选择配置方式。