1. 为什么我们需要JWT鉴权
在传统的Web应用中,最常见的认证方式是使用Session-Cookie机制。服务器在用户登录成功后创建一个Session,将Session ID通过Cookie返回给客户端,后续请求都携带这个Cookie来验证身份。这种方式存在几个痛点:
- 服务器需要存储Session数据,当用户量增大时会对服务器内存造成压力
- 在分布式系统中,需要额外的Session共享机制
- 移动端应用对Cookie的支持不友好
- CSRF攻击风险较高
JWT(JSON Web Token)的出现完美解决了这些问题。它采用无状态的设计,将用户信息直接编码到Token中,服务器只需要验证Token的合法性而无需存储会话信息。这种设计特别适合现代分布式系统和前后端分离架构。
提示:JWT不是用来加密数据的,它的主要作用是签名验证。敏感信息不应该直接放在JWT中。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JWT的结构解析
一个标准的JWT由三部分组成,用点号(.)连接:
code复制header.payload.signature
2.1 Header头部
Header通常由两部分组成:
- typ:令牌类型,这里是JWT
- alg:签名算法,如HS256、RS256等
示例:
json复制{
"alg": "HS256",
"typ": "JWT"
}
这个JSON会被Base64Url编码形成JWT的第一部分。
2.2 Payload负载
Payload包含声明(claims),声明分为三类:
-
注册声明(Registered claims):预定义但不强制使用的标准字段
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):唯一标识
-
公共声明(Public claims):可以自定义,但为避免冲突应注册在IANA JSON Web Token Registry或定义为URI
-
私有声明(Private claims):自定义的声明,用于在同意使用它们的各方之间共享信息
示例:
json复制{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
2.3 Signature签名
签名部分是对前两部分(header和payload)的签名,防止数据篡改。生成签名的算法在header中指定。
以HS256算法为例,签名生成方式如下:
code复制HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
3. JWT的工作流程详解
3.1 登录流程
- 用户提交用户名和密码
- 服务器验证凭证
- 验证通过后,服务器生成JWT并返回给客户端
- 客户端存储JWT(通常在localStorage或Cookie中)
3.2 请求鉴权流程
- 客户端发起请求时携带JWT(通常在Authorization头中)
- 服务器验证JWT签名和有效期
- 验证通过后,服务器处理请求并返回响应
3.3 续签机制
JWT的一个缺点是过期后需要重新登录。常见的续签方案有:
- 双Token机制:同时颁发access_token(短有效期)和refresh_token(长有效期)
- 滑动过期:每次请求都刷新Token有效期
- 静默续签:在Token即将过期时自动续签
示例代码(Node.js实现续签):
javascript复制function refreshToken(oldToken) {
const decoded = jwt.verify(oldToken, secret, {ignoreExpiration: true});
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 300) { // 剩余5分钟时续签
decoded.iat = now;
decoded.exp = now + 3600; // 续签1小时
return jwt.sign(decoded, secret);
}
return oldToken;
}
4. JWT的安全实践
4.1 常见攻击与防御
-
XSS攻击:攻击者通过注入脚本窃取localStorage中的Token
- 防御:设置HttpOnly Cookie、CSP策略
-
CSRF攻击:攻击者诱导用户发起恶意请求
- 防御:SameSite Cookie、CSRF Token
-
重放攻击:攻击者截获Token后重复使用
- 防御:短期有效、使用jti唯一标识
-
算法混淆攻击:攻击者修改alg为"none"
- 防御:在验证时明确指定算法
4.2 最佳实践
- 使用强密钥并定期更换
- 设置合理的过期时间(通常access_token 15分钟-1小时)
- 敏感操作要求二次验证
- 实现Token撤销机制(黑名单或短有效期)
- 不要在JWT中存储敏感信息
4.3 性能优化
- 减少Payload大小
- 使用无状态的设计避免数据库查询
- 考虑使用非对称加密算法(如RS256)将验证密钥与签名密钥分离
5. JWT在分布式系统中的应用
5.1 微服务架构中的JWT
在微服务架构中,JWT特别适合作为服务间认证的方式:
- 网关服务验证JWT后转发给内部服务
- 内部服务可以直接解析JWT获取用户信息而无需重复验证
- 可以实现细粒度的权限控制
5.2 单点登录(SSO)实现
JWT是实现SSO的理想选择:
- 认证中心颁发JWT
- 各子系统验证JWT的有效性
- 用户只需登录一次即可访问所有信任的系统
示例SSO流程:
- 用户访问系统A,未登录重定向到认证中心
- 用户在认证中心登录成功后获取JWT
- 用户携带JWT访问系统A
- 用户访问系统B时,系统B验证同一JWT
5.3 与OAuth2.0的结合
JWT常用作OAuth2.0中的访问令牌:
- 授权服务器颁发JWT格式的access_token
- 资源服务器验证JWT而不需要查询授权服务器
- 可以实现无状态的资源服务器
6. 实战:从零实现JWT鉴权
6.1 Node.js实现示例
安装依赖:
bash复制npm install jsonwebtoken cookie-parser
生成Token:
javascript复制const jwt = require('jsonwebtoken');
const secret = 'your-256-bit-secret';
function generateToken(user) {
return jwt.sign({
userId: user.id,
username: user.username,
role: user.role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1小时过期
}, secret);
}
验证中间件:
javascript复制function authenticate(req, res, next) {
const token = req.cookies.token ||
req.headers['authorization']?.split(' ')[1];
if (!token) return res.sendStatus(401);
try {
const decoded = jwt.verify(token, secret);
req.user = decoded;
next();
} catch (err) {
return res.sendStatus(403);
}
}
6.2 Java Spring Boot实现
添加依赖:
xml复制<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
JWT工具类:
java复制import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
public class JwtUtil {
private static final byte[] SECRET = "your-256-bit-secret".getBytes();
private static final long EXPIRATION = 3600L; // 1小时
public static String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.signWith(Keys.hmacShaKeyFor(SECRET))
.compact();
}
public static boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
6.3 前端集成示例
存储Token:
javascript复制// 登录成功后
const token = response.data.token;
localStorage.setItem('jwt', token);
// 请求时携带
axios.interceptors.request.use(config => {
const token = localStorage.getItem('jwt');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
处理Token过期:
javascript复制axios.interceptors.response.use(response => {
return response;
}, error => {
if (error.response.status === 401) {
// Token过期,跳转登录
window.location.href = '/login';
}
return Promise.reject(error);
});
7. JWT的替代方案与对比
7.1 Session vs JWT
| 特性 | Session | JWT |
|---|---|---|
| 状态 | 有状态 | 无状态 |
| 存储位置 | 服务器内存/数据库 | 客户端 |
| 扩展性 | 需要共享Session | 天然支持分布式 |
| 安全性 | 较好 | 需注意Token安全 |
| 性能 | 每次需查询Session | 只需验证签名 |
| 适用场景 | 传统Web应用 | API/移动端/微服务 |
7.2 Opaque Token
Opaque Token是不透明的令牌,通常是一个随机字符串,服务器需要查询数据库来验证。与JWT相比:
- 更安全:可以随时撤销
- 更灵活:可以包含任何后端逻辑
- 性能较差:每次都需要数据库查询
7.3 PASETO
PASETO(Platform-Agnostic Security Tokens)是JWT的替代方案,解决了JWT的一些安全问题:
- 强制使用强加密算法
- 简化了标准,避免算法混淆攻击
- 更好的默认安全配置
8. 常见问题与解决方案
8.1 Token失效问题
场景:用户修改密码后,之前的Token仍然有效。
解决方案:
- 在Token中加入版本号或密码hash,修改密码时使旧Token失效
- 维护一个短期的黑名单
- 使用短有效期Token配合refresh_token
8.2 多设备登录管理
场景:需要控制同一账户的登录设备数量。
解决方案:
- 在Token中加入设备ID
- 服务器维护设备白名单
- 新登录时使旧设备Token失效
8.3 权限变更同步
场景:用户权限变更后,已颁发的Token仍然具有旧权限。
解决方案:
- 使用短有效期Token
- 在关键操作前重新验证权限
- 实现强制重新登录机制
8.4 性能优化技巧
- 使用非对称加密算法,将验证密钥与签名密钥分离
- 对频繁验证的Token进行短期缓存
- 优化Payload大小,只包含必要信息
- 考虑使用无状态的权限设计
在实际项目中,我通常会建立一个JWT工具类来统一管理这些逻辑,避免散落在各处。对于关键系统,建议实现监控机制来跟踪Token的使用情况,及时发现异常行为。
