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)。
| 12
 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):用于在同意使用它们的各方之间共享信息,既不是注册声明也不是公共声明。 
| 12
 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算法,签名将通过以下方式创建:
| 12
 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认证流程图
| 12
 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:
| 12
 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
| 12
 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
| 12
 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);
 }
 }
 
 | 
| 12
 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
 
 | @Componentpublic 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
| 12
 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
| 12
 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
- 访问令牌过期后,客户端使用刷新令牌获取新的访问令牌
- 如果刷新令牌也过期,用户需要重新登录
| 12
 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指南