JWT技术详解:原理、应用与最佳实践
1. JWT简介
JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。由于数字签名的存在,这些信息是可验证和可信的。JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
1.1 JWT的特点
- 紧凑性:JWT可以通过URL、POST参数或HTTP Header发送,体积小,传输速度快
- 自包含:包含了所有用户所需要的信息,避免了多次查询数据库
- 易于传输:跨语言支持,支持所有主流编程语言
- 安全性:使用数字签名确保信息不被篡改
- 无状态:服务端无需存储会话信息,降低了服务器的负载
1.2 JWT与传统Session的比较
特性 |
JWT |
Session |
存储位置 |
客户端 |
服务端 |
可扩展性 |
高(无状态) |
低(需要会话存储) |
跨域支持 |
原生支持 |
需要额外配置 |
安全性 |
取决于如何使用 |
相对安全 |
性能 |
减少数据库查询 |
需要查询会话信息 |
过期控制 |
需要额外处理 |
内置支持 |
2. JWT的结构
JWT由三部分组成,用点(.)分隔:
这三部分分别是:
- Header(头部)
- Payload(负载)
- Signature(签名)
Header通常由两部分组成:token类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。
1 2 3 4
| { "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):可以由使用JWT的各方定义,但为避免冲突,应在IANA JSON Web Token Registry中定义或使用包含命名空间的URI。
私有声明(Private claims):用于在同意使用它们的各方之间共享信息,既不是注册声明也不是公共声明。
1 2 3 4 5 6 7
| { "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022, "exp": 1516242622 }
|
然后,这个JSON被Base64Url编码形成JWT的第二部分。
2.3 Signature
要创建签名部分,你需要采用编码过的header、编码过的payload,一个秘钥,以及header中指定的算法,然后对其进行签名。
例如,如果你想使用HMAC SHA256算法,签名将通过以下方式创建:
1 2 3 4
| HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
|
签名用于验证消息在传输过程中没有被更改,并且对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所声称的发送方。
3. JWT的工作流程
JWT的典型工作流程如下:
- 用户登录:用户通过用户名和密码进行身份验证
- 服务器生成JWT:验证成功后,服务器使用密钥生成JWT
- 返回JWT给客户端:服务器将JWT返回给客户端
- 客户端存储JWT:客户端将JWT存储在本地(如localStorage或Cookie)
- 请求时附带JWT:客户端在后续请求中,将JWT放在Authorization头中
- 服务器验证JWT:服务器验证JWT的签名和有效期
- 授权访问:验证成功后,允许访问受保护的资源
3.1 JWT认证流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ┌───────────┐ ┌───────────┐ │ │ │ │ │ 客户端 │ │ 服务器 │ │ │ │ │ └───────────┘ └───────────┘ │ │ │ 1. 发送用户名和密码 │ │ ─────────────────────────────────────────>│ │ │ │ 2. 验证用户名和密码 │ │ │ │ 3. 生成JWT │ │ │ │ 4. 返回JWT │ │ <─────────────────────────────────────────│ │ │ │ 5. 存储JWT │ │ │ │ 6. 请求资源(带JWT) │ │ ─────────────────────────────────────────>│ │ │ │ 7. 验证JWT │ │ │ │ 8. 返回受保护资源 │ │ <─────────────────────────────────────────│ │ │
|
4. JWT的实现
4.1 Java实现(使用jjwt库)
4.1.1 添加依赖
Maven:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
|
4.1.2 生成JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.util.Date;
public class JwtUtil { private static final long EXPIRATION_TIME = 86400000; private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(key) .compact(); } public static String validateTokenAndGetUsername(String token) { try { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody() .getSubject(); } catch (Exception e) { return null; } } }
|
4.1.3 在Spring Security中使用JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } String token = header.substring(7); String username = JwtUtil.validateTokenAndGetUsername(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }
|
4.2 Node.js实现(使用jsonwebtoken库)
4.2.1 安装依赖
1
| npm install jsonwebtoken
|
4.2.2 生成JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your-secret-key'; const EXPIRES_IN = '24h';
function generateToken(userId) { return jwt.sign({ id: userId }, SECRET_KEY, { expiresIn: EXPIRES_IN }); }
function verifyToken(token) { try { return jwt.verify(token, SECRET_KEY); } catch (error) { return null; } }
module.exports = { generateToken, verifyToken };
|
4.2.3 在Express中使用JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const express = require('express'); const { generateToken, verifyToken } = require('./jwtUtils'); const app = express();
app.use(express.json());
app.post('/api/login', (req, res) => { const { username, password } = req.body; if (username === 'admin' && password === 'password') { const token = generateToken(1); res.json({ token }); } else { res.status(401).json({ message: '用户名或密码错误' }); } });
function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ message: '未提供token' }); } const user = verifyToken(token); if (!user) { return res.status(403).json({ message: 'token无效或已过期' }); } req.user = user; next(); }
app.get('/api/protected', authenticateToken, (req, res) => { res.json({ message: '这是受保护的资源', user: req.user }); });
app.listen(3000, () => { console.log('服务器运行在端口3000'); });
|
5. JWT的安全性考虑
5.1 JWT的安全风险
- 信息泄露:JWT的payload部分只是Base64编码,不是加密,因此不应在其中存储敏感信息
- 无法撤销:一旦签发,在过期前无法撤销(除非实现黑名单)
- 令牌劫持:如果JWT被窃取,攻击者可以使用它直到过期
- 密钥泄露:如果签名密钥泄露,攻击者可以伪造有效的JWT
5.2 安全最佳实践
- 使用HTTPS:始终通过HTTPS传输JWT,防止中间人攻击
- 设置合理的过期时间:JWT的有效期应尽可能短
- 不存储敏感数据:不要在JWT中存储敏感信息
- 使用强密钥:使用足够长且随机的密钥
- 实现刷新令牌机制:使用短期访问令牌和长期刷新令牌
- 考虑使用黑名单:对于需要立即撤销的情况,实现令牌黑名单
- 验证所有声明:验证issuer、audience、expiration等所有相关声明
- 防止XSS攻击:如果在浏览器中存储JWT,注意防范XSS攻击
- 使用安全的算法:优先选择强加密算法,如RS256而非HS256
5.3 刷新令牌机制
刷新令牌机制是一种常用的安全实践,它使用两种令牌:
- 访问令牌(Access Token):短期有效(如15分钟),用于访问API
- 刷新令牌(Refresh Token):长期有效(如7天),用于获取新的访问令牌
工作流程:
- 用户登录后,服务器返回访问令牌和刷新令牌
- 客户端使用访问令牌访问API
- 访问令牌过期后,客户端使用刷新令牌获取新的访问令牌
- 如果刷新令牌也过期,用户需要重新登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class TokenService { private static final long ACCESS_TOKEN_EXPIRATION = 900000; private static final long REFRESH_TOKEN_EXPIRATION = 604800000; public TokenPair generateTokenPair(String username) { String accessToken = Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION)) .signWith(accessKey) .compact(); String refreshToken = Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION)) .signWith(refreshKey) .compact(); return new TokenPair(accessToken, refreshToken); } public String refreshAccessToken(String refreshToken) { try { String username = Jwts.parserBuilder() .setSigningKey(refreshKey) .build() .parseClaimsJws(refreshToken) .getBody() .getSubject(); return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION)) .signWith(accessKey) .compact(); } catch (Exception e) { return null; } } }
|
6. JWT的应用场景
6.1 认证(Authentication)
JWT最常见的应用场景是用户认证。用户登录后,服务器生成JWT并返回给客户端,客户端在后续请求中携带JWT,服务器验证JWT后允许访问受保护的资源。
JWT可以用于在各方之间安全地传输信息。由于JWT可以签名,接收方可以验证发送方的身份和信息的完整性。
6.3 单点登录(Single Sign On)
JWT可以用于实现跨域的单点登录系统。用户在一个服务上登录后,可以携带JWT访问其他相关服务,而无需重新登录。
6.4 微服务架构
在微服务架构中,JWT可以用于服务间的认证和授权,确保只有经过授权的服务能够访问其他服务的API。
6.5 移动应用
JWT适用于移动应用的认证,因为它不需要在服务器端存储会话信息,减轻了服务器的负担。
7. JWT的局限性与替代方案
7.1 JWT的局限性
- 无法撤销:JWT一旦签发,在过期前无法撤销
- 大小限制:JWT可能比传统的会话ID大,增加了网络传输负担
- 存储开销:客户端需要存储JWT,可能占用更多客户端存储空间
- 无状态的限制:某些应用场景可能需要服务器端状态
7.2 替代方案
- 传统Session:服务器存储会话信息,客户端存储会话ID
- OAuth 2.0:用于授权的开放标准,适用于第三方应用授权
- OpenID Connect:基于OAuth 2.0的身份认证层
- SAML:用于企业级身份认证和单点登录的XML标准
7.3 何时使用JWT
适合使用JWT的场景:
- 需要无状态认证的应用
- 分布式系统和微服务架构
- 跨域认证需求
- API认证
不适合使用JWT的场景:
- 需要即时撤销访问权限的应用
- 存储大量用户状态信息的应用
- 高安全性要求的应用(可能需要额外的安全措施)
8. JWT工具与库
8.1 在线工具
8.2 主流编程语言的JWT库
9. 总结
JWT是一种灵活、安全且高效的认证和信息交换机制,特别适合于现代分布式系统和微服务架构。它通过数字签名确保信息的完整性和真实性,同时减轻了服务器的存储负担。
然而,JWT也有其局限性,如无法即时撤销和潜在的安全风险。因此,在实际应用中,需要根据具体需求选择合适的认证方案,并遵循安全最佳实践。
通过本文的详细介绍,相信读者已经对JWT的原理、应用和最佳实践有了全面的了解,能够在实际项目中正确地使用JWT技术。
参考资料
- JWT官方网站
- RFC 7519 - JSON Web Token
- Auth0 JWT文档
- OWASP JWT安全指南
- Spring Security JWT指南