You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot未用STOMP/SockJS的javax.websocket如何做权限校验

原生JSR356 WebSocket 握手阶段权限校验方案

你当前用的是Spring Boot集成JSR356标准的原生WebSocket实现(未使用STOMP/SockJS),这类端点默认不经过Spring MVC的普通拦截器链,要在连接建立前做角色校验、返回403,直接通过自定义握手拦截器实现即可,不需要改造现有业务逻辑。


实现步骤

1. 编写握手鉴权拦截器

在HTTP升级为WebSocket连接的握手阶段做校验,这个阶段可以拿到完整的HTTP请求/响应对象,校验不通过直接设置403状态码即可阻断连接。

import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Collections;
import java.util.List;

public class AuthHandshakeConfigurator extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 1. 从请求中获取JWT,两种方式二选一即可
        // 方式A:从ws连接的query参数取,连接示例:ws://域名/ws/exam/123?token=xxx
        String token = request.getParameterMap().getOrDefault("token", Collections.emptyList()).stream().findFirst().orElse(null);
        
        // 方式B:从Sec-WebSocket-Protocol请求头取(适合不想把token放url的场景)
        // List<String> protocols = request.getHeaders().get("Sec-WebSocket-Protocol");
        // String token = protocols != null && !protocols.isEmpty() ? protocols.get(0) : null;

        // 2. 校验token合法性、解析用户角色,替换成你自己的JWT解析逻辑即可
        if (token == null || !JwtUtil.validateToken(token)) {
            // 校验不通过直接设置403状态阻断连接
            response.getHeaders().put(HandshakeResponse.HTTP_STATUS_CODE, List.of("403"));
            throw new RuntimeException("未授权访问");
        }
        String userRole = JwtUtil.getUserRoleFromToken(token);
        String userId = JwtUtil.getUserIdFromToken(token);
        // 按业务要求判断角色权限
        if (!"EXAMINEE".equals(userRole)) {
            response.getHeaders().put(HandshakeResponse.HTTP_STATUS_CODE, List.of("403"));
            throw new RuntimeException("无考试访问权限");
        }

        // 校验通过可以把用户信息存在端点配置里,后续@OnOpen方法可以直接读取
        sec.getUserProperties().put("userId", userId);
        sec.getUserProperties().put("userRole", userRole);
        super.modifyHandshake(sec, request, response);
    }
}

注意:浏览器原生WebSocket API不支持自定义普通请求头,如果不想把token拼在URL参数里,就用Sec-WebSocket-Protocol头传递token,握手成功后后端需要在响应头里把这个协议值返回,否则浏览器会报握手失败。

2. 给WebSocket端点绑定鉴权配置

修改你的WebSocket类上的@ServerEndpoint注解,指定刚才写的鉴权配置类,否则拦截器不生效:

import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

@Component
@ServerEndpoint(
    value = "/ws/exam/{tentativeId}",
    configurator = AuthHandshakeConfigurator.class // 绑定自定义鉴权拦截器
)
public class WebSocket {
    private Session session;
    // 注意:直接@Autowired在JSR356模式下会注入失败,修复方法见下一步
    private QuestionRepository questionRepository = SpringContextUtil.getBean(QuestionRepository.class);
    private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
    private static Map<String,Session> sessionPool = new HashMap<String,Session>();

    @OnOpen
    public void onOpen(Session session, @PathParam(value="tentativeId")String tentativeId) {
        // 这里可以直接拿到握手阶段存的用户信息
        String userId = (String) session.getUserProperties().get("userId");
        // 原有业务逻辑保留
        webSockets.add(this);
        this.session = session;
        sessionPool.put(tentativeId, session);
    }

    @OnClose
    public void onClose() {
        webSockets.remove(this);
        System.out.println("[websocket message] disconnected, total:"+webSockets.size());
    }

    @OnMessage
    public void onMessage(String message,@PathParam(value="tentativeId") String tentativeId) throws JsonMappingException, JsonProcessingException {
        // 原有答题、推送下一题逻辑保留
    }

    public void sendAllMessage(String message) {
        for(WebSocket webSocket : webSockets) {
            System.out.println ("[websocket message] broadcast message:"+message);
            try {
                webSocket.session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void sendOneMessage(String tentativeId, String message) {
        Session session = sessionPool.get(tentativeId);
        if (session != null) {
            try {
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3. 修复JSR356模式下Spring Bean注入失败的问题

JSR356的WebSocket实例是每个连接单独创建的,不归Spring容器管理,直接写@Autowired会拿到null,写一个简单的Spring上下文工具类即可解决:

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
}

你原有的WebsocketConfiguration不需要改动,ServerEndpointExporter的Bean保留即可。


前端对接注意点

Vue端发起WebSocket连接时把JWT带上即可,两种传参方式和后端实现对应即可:

  • 拼在URL参数里:
const ws = new WebSocket(`ws://你的服务域名/ws/exam/${tentativeId}?token=${localStorage.getItem('jwt')}`)
  • 用Sec-WebSocket-Protocol传:
const token = localStorage.getItem('jwt')
const ws = new WebSocket(`ws://你的服务域名/ws/exam/${tentativeId}`, [token])

当用户无权限时,浏览器会直接触发ws的onerror和onclose事件,响应状态码为403,可以直接在前端做对应提示。


内容的提问来源于stack exchange,提问作者Ghost

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:09:15