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由三部分组成,用点(.)分隔:

1
xxxxx.yyyyy.zzzzz

这三部分分别是:

  1. Header(头部)
  2. Payload(负载)
  3. Signature(签名)

2.1 Header

Header通常由两部分组成:token类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

然后,这个JSON被Base64Url编码形成JWT的第一部分。

2.2 Payload

Payload包含声明(claims)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:

  1. 注册声明(Registered claims):预定义的声明,建议但不强制使用

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (not before):生效时间
    • iat (issued at):签发时间
    • jti (JWT ID):编号
  2. 公共声明(Public claims):可以由使用JWT的各方定义,但为避免冲突,应在IANA JSON Web Token Registry中定义或使用包含命名空间的URI。

  3. 私有声明(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的典型工作流程如下:

  1. 用户登录:用户通过用户名和密码进行身份验证
  2. 服务器生成JWT:验证成功后,服务器使用密钥生成JWT
  3. 返回JWT给客户端:服务器将JWT返回给客户端
  4. 客户端存储JWT:客户端将JWT存储在本地(如localStorage或Cookie)
  5. 请求时附带JWT:客户端在后续请求中,将JWT放在Authorization头中
  6. 服务器验证JWT:服务器验证JWT的签名和有效期
  7. 授权访问:验证成功后,允许访问受保护的资源

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; // 1天
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); // 用户ID为1
res.json({ token });
} else {
res.status(401).json({ message: '用户名或密码错误' });
}
});

// JWT中间件
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的安全风险

  1. 信息泄露:JWT的payload部分只是Base64编码,不是加密,因此不应在其中存储敏感信息
  2. 无法撤销:一旦签发,在过期前无法撤销(除非实现黑名单)
  3. 令牌劫持:如果JWT被窃取,攻击者可以使用它直到过期
  4. 密钥泄露:如果签名密钥泄露,攻击者可以伪造有效的JWT

5.2 安全最佳实践

  1. 使用HTTPS:始终通过HTTPS传输JWT,防止中间人攻击
  2. 设置合理的过期时间:JWT的有效期应尽可能短
  3. 不存储敏感数据:不要在JWT中存储敏感信息
  4. 使用强密钥:使用足够长且随机的密钥
  5. 实现刷新令牌机制:使用短期访问令牌和长期刷新令牌
  6. 考虑使用黑名单:对于需要立即撤销的情况,实现令牌黑名单
  7. 验证所有声明:验证issuer、audience、expiration等所有相关声明
  8. 防止XSS攻击:如果在浏览器中存储JWT,注意防范XSS攻击
  9. 使用安全的算法:优先选择强加密算法,如RS256而非HS256

5.3 刷新令牌机制

刷新令牌机制是一种常用的安全实践,它使用两种令牌:

  1. 访问令牌(Access Token):短期有效(如15分钟),用于访问API
  2. 刷新令牌(Refresh Token):长期有效(如7天),用于获取新的访问令牌

工作流程:

  1. 用户登录后,服务器返回访问令牌和刷新令牌
  2. 客户端使用访问令牌访问API
  3. 访问令牌过期后,客户端使用刷新令牌获取新的访问令牌
  4. 如果刷新令牌也过期,用户需要重新登录
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; // 15分钟
private static final long REFRESH_TOKEN_EXPIRATION = 604800000; // 7天

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后允许访问受保护的资源。

6.2 信息交换(Information Exchange)

JWT可以用于在各方之间安全地传输信息。由于JWT可以签名,接收方可以验证发送方的身份和信息的完整性。

6.3 单点登录(Single Sign On)

JWT可以用于实现跨域的单点登录系统。用户在一个服务上登录后,可以携带JWT访问其他相关服务,而无需重新登录。

6.4 微服务架构

在微服务架构中,JWT可以用于服务间的认证和授权,确保只有经过授权的服务能够访问其他服务的API。

6.5 移动应用

JWT适用于移动应用的认证,因为它不需要在服务器端存储会话信息,减轻了服务器的负担。

7. JWT的局限性与替代方案

7.1 JWT的局限性

  1. 无法撤销:JWT一旦签发,在过期前无法撤销
  2. 大小限制:JWT可能比传统的会话ID大,增加了网络传输负担
  3. 存储开销:客户端需要存储JWT,可能占用更多客户端存储空间
  4. 无状态的限制:某些应用场景可能需要服务器端状态

7.2 替代方案

  1. 传统Session:服务器存储会话信息,客户端存储会话ID
  2. OAuth 2.0:用于授权的开放标准,适用于第三方应用授权
  3. OpenID Connect:基于OAuth 2.0的身份认证层
  4. SAML:用于企业级身份认证和单点登录的XML标准

7.3 何时使用JWT

适合使用JWT的场景:

  • 需要无状态认证的应用
  • 分布式系统和微服务架构
  • 跨域认证需求
  • API认证

不适合使用JWT的场景:

  • 需要即时撤销访问权限的应用
  • 存储大量用户状态信息的应用
  • 高安全性要求的应用(可能需要额外的安全措施)

8. JWT工具与库

8.1 在线工具

  • jwt.io:用于解码、验证和生成JWT的在线工具
  • JWT Debugger:微软提供的JWT调试工具

8.2 主流编程语言的JWT库

语言 链接
Java jjwt GitHub
JavaScript jsonwebtoken GitHub
Python PyJWT GitHub
PHP firebase/php-jwt GitHub
.NET System.IdentityModel.Tokens.Jwt NuGet
Ruby jwt GitHub
Go golang-jwt/jwt GitHub

9. 总结

JWT是一种灵活、安全且高效的认证和信息交换机制,特别适合于现代分布式系统和微服务架构。它通过数字签名确保信息的完整性和真实性,同时减轻了服务器的存储负担。

然而,JWT也有其局限性,如无法即时撤销和潜在的安全风险。因此,在实际应用中,需要根据具体需求选择合适的认证方案,并遵循安全最佳实践。

通过本文的详细介绍,相信读者已经对JWT的原理、应用和最佳实践有了全面的了解,能够在实际项目中正确地使用JWT技术。

参考资料

  1. JWT官方网站
  2. RFC 7519 - JSON Web Token
  3. Auth0 JWT文档
  4. OWASP JWT安全指南
  5. Spring Security JWT指南