websocket+springboot+springsecurity+springsession结合使用

此篇是在上一篇《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/[email protected]/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.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>

七、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发送给指定人 
*/
@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(); } }

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

方法1

第一种是通过用户名发送,此种方式需要使用登录页面,使用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();

方法2

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

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

方法3

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

总结

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

你可能感兴趣的:(java后台,springboot,springsession,springsecurity,websocket)