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
相关产品推荐
相关产品推荐

