SpringSecurity整合WebSocket并携带token

目的

导入SpringSecurity的SpringBoot项目,在连接WebSocket时进行token校验

实现

SpringBoot整合Websocket的相关知识就不过多赘述,本文主要介绍WebSocket权限校验相关

1. 前端

WebSocket连接

 var windowTag = `${user.id}-${Math.random().toString(36).substr(2)}`;
 var token = user.token;

websocket = new WebSocket(`ws://localhost:9001/ws/chat/${windowTag}`,[token]);

windowTag是生成的随机窗口唯一标识符,token是用户登录后生成的令牌token
当前端发起WebSocket连接请求时,请求头在通信子协议Sec-WebSocket-Protocol里携带token
在这里插入图片描述

2. 后端

前端通过WebSocket的通信子协议携带token发送给后端,现在我们只需要获取到该token就能获取用户信息

/**
  WebSocket配置
*/
@Configuration
public class WebSocketConfig extends ServerEndpointConfig.Configurator {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

    /**
     * 建立握手时,连接前的操作
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 这个userProperties 可以通过 session.getUserProperties()获取
        final Map<String, Object> userProperties = sec.getUserProperties();
        Map<String, List<String>> headers = request.getHeaders();
        List<String> protocol = headers.get(WEBSOCKET_PROTOCOL);
        // 存放自己想要的header信息
        if(protocol != null){
            userProperties.put(WEBSOCKET_PROTOCOL, protocol.get(0));
        }
    }

    /**
     * 初始化端点对象,也就是被@ServerEndpoint所标注的对象
     */
    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
        return super.getEndpointInstance(clazz);
    }

}

将请求头中Sec-WebSocket-Protocol携带的token放入session的userProperties中,方便连接时获取token

@Slf4j
@Component
@ServerEndpoint(value = "/ws/chat/{windowTag}",configurator = WebSocketConfig.class)
public class ChatEndPoint {

    //用线程安全的map来保存当前用户
    private static Map<String, ChatEndPoint> onlineUsers = new ConcurrentHashMap<>();
    //声明一个session对象,通过该对象可以发送消息给指定用户,不能设置为静态,每个ChatEndPoint有一个session才能区分.(websocket的session)
    private Session session;

    //建立连接
    @OnOpen
    public void onOpen(Session session, @PathParam("windowTag") String windowTag){
        this.session = session;
        String username = getUserName(session);
        log.info("上线用户名称: {}", username);
        onlineUsers.put(username + "-" + windowTag, this);
        log.info("在线用户数: {}", onlineUsers.size());
    }

	......

    // 获取用户名
    private String getUserName(Session session){
        String token = getHeader(session, WEBSOCKET_PROTOCOL);
        return new TokenManager().getUserInfoFromToken(token);
    }

	public String getHeader(Session session, String headerName) {
        String header = (String) session.getUserProperties().get(headerName);
        if (StringUtils.isBlank(header)) {
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return header;
    }
}

通过子协议中携带的token获取用户名,并与窗口标识符拼接成连接标识符,之后将连接的session存放进线程安全的Map中

/**
	SpringSecurity的权限校验器
*/
public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;

    public TokenAuthFilter(AuthenticationManager authenticationManager, TokenManager tokenManager) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("根据token获取用户权限并放入security上下文...");
        // websocket需要验证Sec-WebSocket-Protocol中的token
        String token = request.getHeader(WEBSOCKET_PROTOCOL);
        if(token == null){
            token = request.getHeader("token");
        }
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(token);
        //判断如果有权限信息,放到权限上下文中
        if(authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request,response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String token) {
        System.out.println("请求头中的token: " + token);
        if(!token.equals("null")) {
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            System.out.println("token获取用户名:"+username);
            //从token获取对应权限列表
            List<String> permissionValueList = tokenManager.getUserPermissionList(token);
            Collection<GrantedAuthority> authority = new ArrayList<>();
            if(permissionValueList != null){
                for(String permissionValue : permissionValueList) {
                    SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                    authority.add(auth);
                }
            }
            return new UsernamePasswordAuthenticationToken(username,token,authority);
        }
        return null;
    }
}

在SpringSecurity权限校验时先获取Sec-WebSocket-Protocol携带的token
当我们以为一切准备就绪时,运行时发现报错了
在这里插入图片描述
WebSocket握手阶段出错:发送了非空“Sec-WebSocket-Protocol”请求头但是响应中没有此字段。在后端握手时设置一下请求头(Sec-WebSocket-Protocol)即可,前端发来什么值,这里就写什么值

@Order(1)
@Component
@WebFilter(filterName = "WebsocketFilter", urlPatterns = "/ws/**")
public class WebSocketFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String token = ((HttpServletRequest) servletRequest).getHeader(WEBSOCKET_PROTOCOL);
        // 解决 Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
        response.setHeader(WEBSOCKET_PROTOCOL,token);

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {}
}

连接成功!!
在这里插入图片描述
在这里插入图片描述
功能测试
在这里插入图片描述

Spring Cloud中,可以使用Spring Security框架来实现Token的认证和授权,同时也可以使用WebSocket来实现实时通信功能。下面介绍如何在Spring Cloud中使用WebSocket来监听Token过期: 1. 配置WebSocketSpring Cloud项目中,需要配置WebSocket以支持实时通信功能。在配置类上加上@EnableWebSocket注解,然后注册一个WebSocketHandler,用于处理WebSocket连接和消息。 ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("*"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } } ``` 2. 获取TokenWebSocket的处理器中,可以使用Spring Security提供的SecurityContextHolder来获取当前用户的认证信息。从认证信息中可以获取到Token的信息,包括Token的过期时间。 ```java public class MyHandler extends TextWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String token = ((OAuth2Authentication) authentication).getOAuth2Request().getRequestParameters().get("access_token"); OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); if (accessToken == null || accessToken.isExpired()) { // Token已过期,向前端发送一个消息 session.sendMessage(new TextMessage("Token已过期,请重新登录!")); } } } ``` 需要注意的是,这里的OAuth2Authentication和tokenStore需要根据具体的实现进行调整。 3. 实现Token过期监听 在WebSocket的处理器中,需要实现Token过期的监听功能。可以使用Spring Security提供的OAuth2AccessToken来判断Token是否过期,如果过期,则向前端发送一个消息。 ```java public class MyHandler extends TextWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String token = ((OAuth2Authentication) authentication).getOAuth2Request().getRequestParameters().get("access_token"); OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); if (accessToken == null || accessToken.isExpired()) { // Token已过期,向前端发送一个消息 session.sendMessage(new TextMessage("Token已过期,请重新登录!")); } } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (exception instanceof OAuth2AccessTokenExpiredException) { // Token已过期,向前端发送一个消息 session.sendMessage(new TextMessage("Token已过期,请重新登录!")); } else { super.handleTransportError(session, exception); } } } ``` 需要注意的是,这里的OAuth2AccessTokenExpiredException是Spring Security框架提供的异常类,用于表示Token已过期。当WebSocket连接出现异常时,可以通过捕获OAuth2AccessTokenExpiredException来判断Token是否过期。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值