spring 集成websocket,接口校验token时,遇到个问题,记录一下;
1、前端JS跨域请求,携带cookie,校验token,
Android手机端,有的内置浏览器,跨域请求,携带cookie失效,导致无法校验token;
浏览器同源策略:
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://www.cnblogs.com/laixiangran/p/9064769.html
登录地址:https://www.test.com/account/login.action 登录成功后,服务器返回响应,cookie中设置token信息
websocket地址:wss://www.test.com/websocket/monitor ==》不同源
使用HBuilder开发APP,发现,在有些Android手机上,不能正常携带cookie信息(包含token);
(PC 浏览器端,同样的JS代码,携带cookie信息正常,暂时没有发现问题;)
所以,参考网上的解决方法,移动端,在请求websocket地址后,添加参数:
wss://www.test.com/websocket/monitor?token=xxxxxxxxxx
对于服务端,则支持两种方式请求:1、优先从cookie中获取token;
2、如果cookie中没有token,则从请求参数中获取token;
3、否则,无token;
2、服务端校验token的时机
2.1、在websocket handshake 前 校验
WebSocketConfig.java 配置
@Configuration
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
private static final Logger logger = Logger.getLogger(WebSocketConfig.class);
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
logger.info("registerWebSocketHandlers");
// 前台 可以使用websocket环境
registry.addHandler(newWebSocketHandler(), "/websocket/monitor")
.setHandshakeHandler(newHandshakeHandler())
.setAllowedOrigins("*")// 允许跨域
.addInterceptors(newHandshakeInterceptor());
// 前台 不可以使用websocket环境,则使用sockjs进行模拟连接
registry.addHandler(newWebSocketHandler(), "/sockjs/websocket")
.setAllowedOrigins("*")
.addInterceptors(newHandshakeInterceptor())
.withSockJS();
}
public WebSocketHandler newWebSocketHandler() {
return new MyWebSocketHandler();
}
public HandshakeInterceptor newHandshakeInterceptor() {
return new WebSocketHandshakeInterceptor();
}
public HandshakeHandler newHandshakeHandler() {
return new MyHandshakeHandler();
}
}
public class WebSocketHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
private static final Logger LOGGER = Logger.getLogger(WebSocketHandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map<String, Object> attributes)
throws Exception {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String keyName = "token";
// 1.从cookie中获取token
String token = CookieUtils.getCookieValue(servletRequest, keyName);
if (!StringUtils.isEmpty(token)) {
// 2.从请求参数中获取token
token = servletRequest.getParameter(keyName);
}
// 3.校验token
if (StringUtils.isEmpty(token)) {
LOGGER.error("no user token, refuse connect");
response.setStatusCode(HttpStatus.FORBIDDEN);
// attributes.put("selfErrorCode", 10000009);
return false;
}
return super.beforeHandshake(request, response, webSocketHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception e) {
super.afterHandshake(request, response, wsHandler, e);
}
}
但是,使用该种方法,前端被拒绝后,JS代码 WebSocket.onerror,WebSocket.onclose 回调方法,都无法获取到对应的错误码,进而无法区分错误;
(TODO 也有可能是方法不对吧,有空再找找原因)
2.2、在websocket handshake 时 校验
MyHandshakeHandler.java,握手时校验,继承自DefaultHandshakeHandler;
或者,也可以自己实现接口 HandshakeHandler ,实现 doHandshake 方法;
public class MyHandshakeHandler extends DefaultHandshakeHandler {
private static final Logger LOGGER = Logger.getLogger(MyHandshakeHandler.class);
@Override
protected boolean isValidOrigin(ServerHttpRequest request) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String keyName = "token";
// 1.从cookie中获取token
String token = CookieUtils.getCookieValue(servletRequest, keyName);
if (!StringUtils.isEmpty(token)) {
// 2.从请求参数中获取token
token = servletRequest.getParameter(keyName);
}
// 3.校验token
if (StringUtils.isEmpty(token)) {
LOGGER.error("no user token, refuse connect");
return false;
}
return super.isValidOrigin(request);
}
}
在AbstractHandshakeHandler的doHandshake方法中,会调用 isValidOrigin 方法,如果校验token失败,则会返回 FORBIDDEN 403 错误;
/** * Return whether the request {@code Origin} header value is valid or not. * By default, all origins as considered as valid. Consider using an * {@link OriginHandshakeInterceptor} for filtering origins if needed. */ protected boolean isValidOrigin(ServerHttpRequest request) { return true; }
/** * {@code 403 Forbidden}. * @see <a href="http://tools.ietf.org/html/rfc7231#section-6.5.3">HTTP/1.1: Semantics and Content, section 6.5.3</a> */ FORBIDDEN(403, "Forbidden"),
同上,前端JS无法获取错误码;
2.3、在websocket handshake 后 ,建立连接时 校验 (有点晚了)
MyWebSocketHandler.java,在 afterConnectionEstablished 方法中校验;
这种方式,前端JS,WebSocket.onclose 可以获取错误码
不过,体验上不太好(先连接成功,然后断开);
public class MyWebSocketHandler implements WebSocketHandler {
private static final Logger LOGGER = Logger.getLogger(MyWebSocketHandler.class);
private static final List<WebSocketSession> sessionList = new LinkedList<>();
private static final List<String> tokenList = new LinkedList<>();
private String getTokenFromQueryParams(WebSocketSession session) {
String queryParams = session.getUri().getQuery();
if (StringUtils.isEmpty(queryParams)) {
return null;
}
// 测试,只有一个参数
String[] array = queryParams.split("=");
if (array.length == 2) {
return array[1];
}
return null;
}
private String getTokenFromCookie(WebSocketSession session) {
HttpHeaders httpHeaders = session.getHandshakeHeaders();
return CookieUtils.getCookieValue(httpHeaders, "token");
}
private String getTokenBySession(WebSocketSession session) {
String token = getTokenFromCookie(session);
if (!StringUtils.isEmpty(token)) {
return token;
}
return getTokenFromQueryParams(session);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String token = getTokenBySession(session);
if (StringUtils.isEmpty(token)) {
LOGGER.error("token is empty");
session.close(new CloseStatus(4500));
return;
}
// 添加session
synchronized (sessionList) {
sessionList.add(session);
}
}