通过这节内容,我们将了解到以下内容
- 怎么对websocket连接鉴权,不是任意一个连接我都要去建立连接的?
- 用户鉴权以后,这个用户就算“登录”成功了。用户的会话怎么来管理。我想给该用户推送一条消息,怎么找到该用户?
一、什么是jwtToken
简单来说,token就是一种身份验证方法,和cookie有相似作用;它被很多人翻译过来后生动的称为“令牌”,它的扩展性,安全性更高,非常适合用在Web应用和移动开发应用上。
1.1token验证流程
使用token身份验证,服务器端就不会存储用户的登录记录。
- (1)客户端使用用户名跟密码请求登录;
- (2)服务端收到请求,去验证用户名与密码;
- (3)验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
- (4)客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
- (5)客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
- (6)服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。
Token有很多种,下面重点介绍Jwt
1.2Jwt
json web token(JWT)是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。它是以JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
http协议无状态的,所以需要sessionId或token的鉴权机制,jwt的token认证机制不需要在服务端再保留用户的认证信息或会话信息。这就意味着基于jwt认证机制的应用程序不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,jwt更适用于分布式应用
1.3jwt的组成部分
标准的jwt令牌分为三部分,分别是Header、payload、signature;
1.3.1Header
它的组成部分包括两点
参数类型-jwt
签名的算法-hs256
最后会经过Base64加密方式进行编码,看起来是这个样子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
1.3.2 Payload
它的组成就是登陆用户的一些信息,和token发行和失效时间等;这些内容里面有一些是标准字段,你也可以添加其它需要的内容。
iss:Issuer,发行者
sub:Subject,主题
aud:Audience,观众
exp:Expiration time,过期时间
nbf:Not before
iat:Issued at,发行时间
jti:JWT ID
最后它也会通过Base64加密方式进行编码,仍然长的像上面的样子
1.3.3 Signature
它是由3个部分组成,先是用 Base64 编码的 header 和 payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。
secret就是在最后第二次加密时加的盐,算是一个秘钥(只保留在服务器),不向外部透露。
来看一下最后的jwt完整版
eyJhbGciOiJIUzI1NiIsInR9cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
kpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
8HILod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI
二、用jwtToken建立连接代码示例
2.1 token获取接口
我们可以定义一个接口,入参是userId。生成jwtToken,具体token的生成算法略。
@Data
public class TokenRequest {
private String userId;
}
@RestController
@Slf4j
@RequestMapping("/api/ws/token")
@Api("token获取")
public class TokenController {
@PostMapping("/get")
public String getToken(@RequestBody @Validated TokenRequest tokenRequest) {
return TokenUtil.generateToken(tokenRequest.getUserId());
}
}
前端页面调用此接口,获取到token
此时前端建立连接,可以将此token带上
2.2 前端连接示例
stompClient.connect方法中,将header带入
//建立连接
function connect() {
//连接请求头里面,设置好我们提前获取好的token
var headers = {
token: ‘eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMDAxIiwiZXhwIjoxNzE0MTQwNzI3LCJpYXQiOjE3MTQxMzM1Mjd9.QBTagYepIuZs8C7xmxsizu4jKgOxg_z0Q13Y8LvbtnA’
};
var url = endpoint;
var socket = new SockJS(url);
stompClient = Stomp.over(socket);
//建立连接
stompClient.connect(headers, function (msg) {
});
}
2.3 后端校验逻辑
后端在接收客户端的消息时,会通过inboundChannel。Spring给我们提供了消息处理的拦截器。通过拦截器,我们就可以获取到收到的消息,进行前置处理,比如用户校验等
拦截器如何注册的?
@Configuration
@EnableWebSocketMessageBroker
@EnableConfigurationProperties({UserSessionProperties.class})
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer, ApplicationListener<BrokerAvailabilityEvent> {
private final WebSocketInboundInterceptor webSocketInboundInterceptor;
public WebSocketConfig(WebSocketInboundInterceptor webSocketInboundInterceptor) {
this.webSocketInboundInterceptor = webSocketInboundInterceptor;
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(webSocketInboundInterceptor);
}
}
我们定义了WebSocketInboundInterceptor ,将其注册上了。其实自定义实现如下:
@Component
public class WebSocketInboundInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor == null) {
return message;
}
//当消息是CONNECT连接报文时,进行会话校验,建立连接
if (Objects.equals(accessor.getCommand(), StompCommand.CONNECT)) {
connect(message, accessor);
}
return message;
}
/**
* 建立会话
*
* @param message
* @param accessor
*/
private void connect(Message<?> message, StompHeaderAccessor accessor) {
//1通过请求头获取到token
String token = accessor.getFirstNativeHeader(WsConstants.TOKEN_HEADER);
//2如果token为空或者用户id没有解析出来,抛出异常,spring会将此websocket连接关闭
if (StringUtils.isEmpty(token)) {
throw new MessageDeliveryException("token missing!");
}
String userId = TokenUtil.parseToken(token);
if (StringUtils.isEmpty(userId)) {
throw new MessageDeliveryException("userId missing!");
}
//这个是每个会话都会有的一个sessionId
String simpleSessionId = (String) message.getHeaders().get(SimpMessageHeaderAccessor.SESSION_ID_HEADER);
//3创建自己的业务会话session对象
UserSession userSession = new UserSession();
userSession.setSimpleSessionId(simpleSessionId);
userSession.setUserId(userId);
userSession.setCreateTime(LocalDateTime.now());
//4关联用户的会话。通过msgOperations.convertAndSendToUser(username, "/topic/subNewMsg", msg); 此方法,可以发送给用户消息
accessor.setUser(new UserSessionPrincipal(userSession));
}
}
代码里面通过判断CONNECT连接报文,就进行会话的校验和会话的绑定。具体看下代码中的注释。
其中UserSessionPrincipal定义如下
public class UserSessionPrincipal implements Principal {
private final UserSession userSession;
public UserSessionPrincipal(UserSession userSession) {
this.userSession = userSession;
}
@Override
public String getName() {
return userSession.getUserId();
}
}
通过 accessor.setUser(new UserSessionPrincipal(userSession))进行会话绑定以后,我们就可以用我们的业务的userId给指定用户发送消息了!
上面示例,我们token解析出来的用户id是1001,那业务代码就可以通过该方法发送给指定用户消息了。
msgOperations.convertAndSendToUser(“1001”, "/topic/answer", msg);
本节的示例源码,都在开源项目中:文章链接【stomp实战】搭建一套websocket推送平台。文章最后有项目地址。