关于websocket的请求必须登录,实现websocket需要登录后才可使用,不登录不能建立连接。
后台spring security配置添加websocket的请求可以匿名访问,关于websocket的请求不要认证就可以随意访问,去除匿名访问后,前端在与websocket建立链接无法在请求头里直接加入Authorization token信息,任何关于websocket的请求都无法通过token认证。
解决办法: 使用websocket的Sec-WebSocket-Protocol参数,将token传回后台,后台借助HttpServletRequestWrapper重新生成新的请求信息,实现使用一套登录认证拦截
原理:
HttpServletRequestWrapper 采用装饰者模式对HttpServletRequest进行包装,我们可以通过继承HttpServletRequestWrapper 类去重写getParameterValues,getParameter等方法,实际还是调用
HttpServletRequest的相对应方法,但是可以对方法的结果进行改装。
1、前端webSock.js
与webSocke建立连接
var websock = null;var global_callback = null;
var serverPort = "8080"; // webSocket连接端口
var wsurl = "ws://" + window.location.hostname + ":" + serverPort+"/websocket/message";/*** 创建socket连接* @param callback*/
function createWebSocket(callback) {if (websock == null || typeof websock !== WebSocket) {initWebSocket(callback);}
}/*** 初始化socket* @param callback*/
function initWebSocket(callback) {global_callback = callback;// 初始化websocket//token会放在请求的Sec-WebSocket-Protocol参数里面websock = new WebSocket(wsurl,['你的token']);websock.onmessage = function (e) {websocketOnMessage(e);};websock.onclose = function (e) {websocketClose(e);};websock.onopen = function () {websocketOpen();};// 连接发生错误的回调方法websock.onerror = function () {console.log("WebSocket连接发生错误");};
}/*** 实际调用的方法*/
function sendSock(agentData ) {if (websock.readyState === websock.OPEN) {// 若是ws开启状态websocketsend(agentData);} else if (websock.readyState === websock.CONNECTING) {// 若是 正在开启状态,则等待1s后重新调用setTimeout(function () {sendSock(agentData);}, 1000);} else {// 若未开启 ,则等待1s后重新调用setTimeout(function () {sendSock(agentData);}, 1000);}
}/*** 关闭*/
function closeSock() {websock.close();
}/*** 数据接收*/
function websocketOnMessage(msg) {// 收到信息为Blob类型时let result = null;// debuggerif (msg.data instanceof Blob) {const reader = new FileReader();reader.readAsText(msg.data, "UTF-8");reader.onload = (e) => {result = reader.result;// console.log("websocket收到", result);global_callback(result);};} else {result = msg.data;// console.log("websocket收到", result);global_callback(result);}
}/*** 数据发送*/
function websocketsend(agentData) {// console.log("发送数据:" + agentData);websock.send(agentData);
}/*** 异常关闭*/
function websocketClose(e) {// console.log("connection closed (" + e.code + ")");
}function websocketOpen(e) {// console.log("连接打开");
}export { sendSock, createWebSocket, closeSock };
2、后端的JWT拦截
前提 spring security配置类不允许/websocket匿名访问,需要鉴权才可以使用
2.1 先实现 HeaderMapRequestWrapper继承HttpServletRequestWrapper
来实现修改HttpServletRequest的请求参数
/*** 修改header信息,key-value键值对儿加入到header中*/
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {private static final Logger LOGGER = LoggerFactory.getLogger(HeaderMapRequestWrapper.class);/*** construct a wrapper for this request** @param request*/public HeaderMapRequestWrapper(HttpServletRequest request) {super(request);}private Map<String, String> headerMap = new HashMap<>();/*** add a header with given name and value** @param name* @param value*/public void addHeader(String name, String value) {headerMap.put(name, value);}@Overridepublic String getHeader(String name) {String headerValue = super.getHeader(name);if (headerMap.containsKey(name)) {headerValue = headerMap.get(name);}return headerValue;}/*** get the Header names*/@Overridepublic Enumeration<String> getHeaderNames() {List<String> names = Collections.list(super.getHeaderNames());for (String name : headerMap.keySet()) {names.add(name);}return Collections.enumeration(names);}@Overridepublic Enumeration<String> getHeaders(String name) {List<String> values = Collections.list(super.getHeaders(name));if (headerMap.containsKey(name)) {values = Arrays.asList(headerMap.get(name));}return Collections.enumeration(values);}
}
token过滤器 JWT认证拦截器修改
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException{/*** 通过request的url判断是否为websocket请求* 如果为websocket请求,先处理Authorization*/if(request.getRequestURI().contains("/websocket")){HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);//修改header信息 将websocket请求中的Sec-WebSocket-Protocol值取出处理后放入认证参数key//此处需要根据自己的认证方式进行修改 Authorization 和 BearerrequestWrapper.addHeader("Authorization","Bearer "+request.getHeader("Sec-WebSocket-Protocol"));request = (HttpServletRequest) requestWrapper;}//处理完后就可以走系统正常的登录认证权限认证逻辑了//下边就是自己的验证token 登陆逻辑LoginUser loginUser = getLoginUser(request);·······chain.doFilter(request, response);}
websocket如何携带header或参数
var ws = new WebSocket("ws://url?userid=1");
ebsocket如何携带header
最近项目组接到新需求,需要websocket连接时,在header里面传递token,由于token较长,不适合在url中直接拼接。
网上查阅了相关的资料,websocket没有像http那样可以只定义请求头的一些参数,只有一个Sec-WebSocket-Protocol属性用于自定义子协议。
意思就是说可以将token当做一个参数传递给后端,只不过参数的封装形式是以Sec-WebSocket-Protocol为key的一个header属性值。
后台接收到websocket连接后,可以通过下述代码获取到token值。
request.getHeader("Sec-WebSocket-Protocol")
需要注意的是,如果前端通过websocket连接时指定了Sec-WebSocket-Protocol,后端接收到连接后,必须原封不动的将Sec-WebSocket-Protocol头信息返回给前端,否则连接会抛出异常。
前端代码格式
let WebSocket = new WebSocket(url,[protocols]);
url
要连接的URL;WebSocket服务器响应的URL。
protocols 可选
一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个WebSocket子协议(例如,您可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。
const webSocket = new WebSocket(url,[token]);
后端依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
java代码
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Objects;/*** webSocket握手拦截器* @since 2023年12月16日16:57:52*/
@Component
public class PortalHandshakeInterceptor implements HandshakeInterceptor {private final Logger logger = LoggerFactory.getLogger(PortalHandshakeInterceptor.class);@Resourceprivate UserClient userClient;/*** 初次握手访问前* @param request* @param serverHttpResponse* @param webSocketHandler* @param map* @return*/@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) {logger.info("HandshakeInterceptor beforeHandshake start...");if (request instanceof ServletServerHttpRequest) {HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();String authorization = req.getHeader("Sec-WebSocket-Protocol");logger.info("authorization = {}", authorization);if (Objects.isNull(userClient)){userClient = (UserClient) SpringContextUtil.getBean("userClient");}TokenVO tokenVO = userClient.checkToken(authorization, authorization);Long userId = tokenVO.getUserId();logger.info("userId = {}, tokenVO = {}", userId, tokenVO);if (Objects.isNull(userId)){serverHttpResponse.setStatusCode(HttpStatus.FORBIDDEN);logger.info("【beforeHandshake】 authorization Parse failure. authorization = {}", authorization);return false;}//存入数据,方便在hander中获取,这里只是在方便在webSocket中存储了数据,并不是在正常的httpSession中存储,想要在平时使用的session中获得这里的数据,需要使用session 来存储一下map.put(MagicCode.WEBSOCKET_USER_ID, userId);map.put(MagicCode.WEBSOCKET_CREATED, System.currentTimeMillis());map.put(MagicCode.TOKEN, authorization);map.put(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME, req.getSession().getId());logger.info("【beforeHandshake】 WEBSOCKET_INFO_MAP: {}", map);}logger.info("HandshakeInterceptor beforeHandshake end...");return true;}/*** 初次握手访问后,将前端自定义协议头Sec-WebSocket-Protocol原封不动返回回去,否则会报错* @param request* @param serverHttpResponse* @param webSocketHandler* @param e*/@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {logger.info("HandshakeInterceptor afterHandshake start...");HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();HttpServletResponse httpResponse = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();if (StringUtils.isNotEmpty(httpRequest.getHeader("Sec-WebSocket-Protocol"))) {httpResponse.addHeader("Sec-WebSocket-Protocol", httpRequest.getHeader("Sec-WebSocket-Protocol"));}logger.info("HandshakeInterceptor afterHandshake end...");}
}