Session 和 JWT(JSON Web Token)都是用于在用户和服务器之间建立认证状态的机制,但它们在工作原理、存储方式和安全性等方面存在着一些差异,下面我们一起来看。
1.什么是JWT?
Session 我们已经很熟悉了,那什么是 JWT 呢?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络上安全传输信息的简洁、自包含的方式。它通常被用于身份验证和授权机制。
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
- 头部(Header):包含了关于生成该 JWT 的信息以及所使用的算法类型。
- 载荷(Payload):包含了要传递的数据,例如身份信息和其他附属数据。JWT 官方规定了 7 个字段,可供使用:
- iss (Issuer):签发者。
- sub (Subject):主题。
- aud (Audience):接收者。
- exp (Expiration time):过期时间。
- nbf (Not Before):生效时间。
- iat (Issued At):签发时间。
- jti (JWT ID):编号。
- 签名(Signature):使用密钥对头部和载荷进行签名,以验证其完整性。
JWT 官网:https://jwt.io/
2.JWT优点分析
JWT 相较于传统的基于会话(Session)的认证机制,具有以下优势:
- 无需服务器存储状态:传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。
- 跨域支持:由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。
- 适应微服务架构:在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
- 自包含:JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
- 扩展性:JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。
总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。
3.JWT基本使用
在 Java 开发中,可以借助 JWT 工具类来方便的操作 JWT,例如 HuTool 框架中的 JWTUtil。
HuTool 介绍:https://doc.hutool.cn/pages/JWTUtil/
使用 HuTool 操作 JWT 的步骤如下:
- 添加 HuTool 框架依赖
- 生成 Token
- 验证和解析 Token
(1)添加 HuTool 框架依赖
在 pom.xml 中添加以下信息:
cn.hutool
hutool-all
5.8.16
(2)生成 Token
Map map = new HashMap() {
private static final long serialVersionUID = 1L;
{
put("uid", Integer.parseInt("123")); // 用户ID
put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15); // 过期时间15天
}
};
JWTUtil.createToken(map, "服务器端秘钥".getBytes());
(3)验证和解析 Token
验证 Token 的示例代码如下:
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MjQwMDQ4MjIsInVzZXJJZCI6MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV_op5LoibLkuozlj7ciLCJzeXNfbWVudV8xIiwiUk9MRV_op5LoibLkuIDlj7ciLCJzeXNfbWVudV8yIl0sImp0aSI6ImQ0YzVlYjgwLTA5ZTctNGU0ZC1hZTg3LTVkNGI5M2FhNmFiNiIsImNsaWVudF9pZCI6ImhhbmR5LXNob3AifQ.aixF1eKlAKS_k3ynFnStE7-IRGiD5YaqznvK2xEjBew";
JWTUtil.verify(token, "123456".getBytes());
解析 Token 的示例代码如下:
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.U2aQkC2THYV9L0fTN-yBBI7gmo5xhmvMhATtu8v0zEA";
final JWT jwt = JWTUtil.parseToken(rightToken);
jwt.getHeader(JWTHeader.TYPE);
jwt.getPayload("sub");
(4)代码实战
在登录成功之后,生成 Token 的示例代码如下:
// 登录成功,使用 JWT 生成 Token
Map payload = new HashMap() {
private static final long serialVersionUID = 1L;
{
put("uid", userinfo.getUid());
put("manager", userinfo.getManager());
// JWT 过期时间为 15 天
put("exp", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
}
};
String token = JWTUtil.createToken(payload, AppVariable.JWT_KEY.getBytes());
例如在 Spring Cloud Gateway 网关中验证 Token 的实现代码如下:
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.example.common.AppVariable;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
// 排除登录验证的 URL 地址
private String[] skipAuthUrls = {"/user/add", "/user/login"};
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 当前请求的 URL
String url = exchange.getRequest().getURI().getPath();
for (String item : skipAuthUrls) {
if (item.equals(url)) {
// 继续往下走
return chain.filter(exchange);
}
}
ServerHttpResponse response = exchange.getResponse();
// 登录判断
List tokens =
exchange.getRequest().getHeaders().get(AppVariable.TOKEN_KEY);
if (tokens == null || tokens.size() == 0) {
// 当前未登录
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// token 有值
String token = tokens.get(0);
// JWT 效验 token 是否有效
boolean result = false;
try {
result = JWTUtil.verify(token, AppVariable.JWT_KEY.getBytes());
} catch (Exception e) {
result = false;
}
if (!result) {
// 无效 token
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
} else { // 判断 token 是否过期
final JWT jwt = JWTUtil.parseToken(token);
// 得到过期时间
Object expObj = jwt.getPayload("exp");
if (expObj == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
long exp = Long.parseLong(expObj.toString());
if (System.currentTimeMillis() > exp) {
// token 过期
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 值越小越早执行
return 1;
}
}
4.JWT实现原理
JWT 本质是将秘钥存放在服务器端,并通过某种加密手段进行加密和验证的机制。加密签名=某加密算法(header+payload+服务器端私钥),因为服务端私钥别人不能获取,所以 JWT 能保证自身其安全性。
5.Session VS JWT
Session 和 JWT 的区别主要有以下几点:
- 工作原理不同:Session 机制依赖于服务器端的存储。当用户首次登录时,服务器会创建一个会话,并生成一个唯一的会话 ID,然后将这个 ID 返回给客户端(通常是通过Cookie)。客户端在后续的请求中会携带这个会话 ID,服务器根据会话ID来识别用户并获取其会话信息;而 JWT 是一种无状态的认证机制,它通过在客户端存储令牌(Token)来实现认证。当用户登录时,服务器会生成一个包含用户信息和有效期的 JWT,并将其返回给客户端。客户端在后续的请求中会携带这个 JWT,服务器通过验证 JWT 的有效性来识别用户。
- 存储方式不同:Session 信息存储在服务器端,通常是保存在内存或数据库中。这种方式需要服务器维护会话状态,因此在分布式系统或微服务架构中,会话信息的共享和同步可能会成为问题;而 JWT信息存储在客户端,通常是保存在浏览器的本地存储或 HTTP 请求的头部中。这种方式无需服务器维护会话状态,使得 JWT 在分布式系统或微服务架构中更加灵活和易于扩展。
- 有效期和灵活性不同:Session 的有效期通常由服务器控制,并且在会话期间用户状态可以在服务器端动态改变。但这也意味着服务器需要管理会话的生命周期;而 JWT 的有效期可以在令牌生成时设置,并且可以在客户端进行缓存和重复使用。这使得 JWT 在需要频繁访问资源且不需要频繁更改用户状态的场景中更加适用。此外,JWT 还支持在令牌中包含自定义的用户信息,提供了更大的灵活性。
课后思考
既然 JWT 的有效期是在令牌生成时设置的,那如何实现 JWT 的自动续期呢?又如何将已经泄漏的 JWT 令牌作废呢?