Java JWT 完整教程
目录
1. 什么是 JWT
1.1 JWT 简介
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。
1.2 传统认证 vs Token 认证
| 传统 Session 认证 | Token 认证 |
|------------------|-----------|
| 服务端存储 Session ID | 无状态,服务端不存储 |
| 需要 Session 存储层 | Token 自包含用户信息 |
| 扩展性差 | 易于水平扩展 |
| 跨域支持困难 | 天然支持跨域 |
1.3 为什么使用 JWT?
-
无状态: 降低服务器负载
-
可扩展: 适合分布式/云架构
-
跨域友好: 适用于微服务架构
-
自包含: Token 中包含所有必要信息
2. JWT 结构详解
JWT 由三部分组成,用点 (.) 分隔:
[Header].[Payload].[Signature]
示例 Token
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
2.1 Header(头部)
Base64 解码后:
{
"alg": "HS256",
"typ": "JWT"
}
-
alg: 签名算法(如 HS256、RS256)
-
typ: Token 类型
2.2 Payload(载荷)
Base64 解码后:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1466633317,
"exp": 1466636917
}
标准声明(Registered Claims)
| 声明 | 全称 | 说明 |
|-----|------|-----|
| iss | Issuer | 签发者 |
| sub | Subject | 主题(通常是用户ID) |
| aud | Audience | 接收方 |
| exp | Expiration | 过期时间 |
| nbf | Not Before | 生效时间 |
| iat | Issued At | 签发时间 |
| jti | JWT ID | 唯一标识 |
2.3 Signature(签名)
签名计算方式:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
⚠️ 重要: 签名只能验证数据完整性,不能加密数据。JWT 内容是 Base64 编码的,任何人都可以解码查看。
3. JJWT 库使用
3.1 添加依赖
<!-- pom.xml -->
<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>
3.2 生成 Token
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 Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Token 有效期:10小时
public static final long TOKEN_VALIDITY = 10 * 60 * 60 * 1000;
/**
* 生成 JWT Token
*/
public String generateToken(String username) {
Date now = new Date();
Date expiration = new Date(now.getTime() + TOKEN_VALIDITY);
return Jwts.builder()
.setSubject(username) // 设置主题(用户名)
.setIssuer("your-app") // 设置签发者
.setIssuedAt(now) // 设置签发时间
.setExpiration(expiration) // 设置过期时间
.claim("role", "admin") // 自定义声明
.signWith(SECRET_KEY) // 签名
.compact(); // 生成紧凑格式
}
}
3.3 解析和验证 Token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
public class JwtUtil {
/**
* 解析 JWT Token
*/
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 从 Token 中获取用户名
*/
public String getUsernameFromToken(String token) {
return parseToken(token).getSubject();
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token, String username) {
try {
Claims claims = parseToken(token);
String tokenUsername = claims.getSubject();
Date expiration = claims.getExpiration();
return tokenUsername.equals(username)
&& expiration.after(new Date());
} catch (JwtException e) {
// Token 无效或已被篡改
return false;
}
}
}
3.4 完整的 TokenManager 类
package com.example.jwt.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class TokenManager {
public static final long TOKEN_VALIDITY = 10 * 60 * 60; // 10小时(秒)
@Value("${jwt.secret}")
private String jwtSecret;
/**
* 生成 JWT Token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 可以添加自定义声明
claims.put("roles", userDetails.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证 Token
*/
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 从 Token 获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 从 Token 获取过期时间
*/
public Date getExpirationFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 检查 Token 是否过期
*/
private boolean isTokenExpired(String token) {
Date expiration = getExpirationFromToken(token);
return expiration.before(new Date());
}
/**
* 获取指定声明
*/
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 获取所有声明
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 获取签名密钥
*/
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
4. Spring Boot 集成
4.1 项目配置
application.properties
spring.application.name=jwt-demo
jwt.secret=你的Base64编码的密钥(至少64字符)
4.2 JWT 过滤器
package com.example.jwt.filter;
import com.example.jwt.service.JwtUserDetailsService;
import com.example.jwt.util.TokenManager;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 从请求头获取 Authorization
String authHeader = request.getHeader("Authorization");
String username = null;
String token = null;
// 检查是否是 Bearer Token
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
try {
username = tokenManager.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
System.out.println("无法获取 JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token 已过期");
}
}
// 验证 Token 并设置认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenManager.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
4.3 Security 配置
package com.example.jwt.config;
import com.example.jwt.filter.JwtFilter;
import com.example.jwt.security.JwtAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(使用 JWT 不需要)
.csrf(csrf -> csrf.disable())
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册接口放行
.requestMatchers("/api/public/**").permitAll() // 公开接口放行
.anyRequest().authenticated() // 其他接口需要认证
)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint)
)
// 无状态 Session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 添加 JWT 过滤器
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
4.4 登录控制器
package com.example.jwt.controller;
import com.example.jwt.dto.LoginRequest;
import com.example.jwt.dto.LoginResponse;
import com.example.jwt.service.JwtUserDetailsService;
import com.example.jwt.util.TokenManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@CrossOrigin
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
try {
// 认证用户
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
} catch (BadCredentialsException e) {
throw new RuntimeException("用户名或密码错误");
}
// 生成 Token
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = tokenManager.generateToken(userDetails);
return ResponseEntity.ok(new LoginResponse(token));
}
}
5. 安全最佳实践
5.1 常见攻击与防护
5.1.1 None 算法攻击
问题: 攻击者将算法改为 none,绕过签名验证。
防护:
// 明确指定期望的算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
.build();
5.1.2 Token 劫持(Sidejacking)
问题: Token 被拦截后,攻击者可以冒充用户。
防护: 添加用户上下文指纹
// 生成随机指纹
String fingerprint = generateRandomString();
// 将指纹哈希存入 Token
String fingerprintHash = sha256(fingerprint);
String token = Jwts.builder()
.claim("fingerprint", fingerprintHash)
// ...
.compact();
// 将原始指纹存入 HttpOnly Cookie
Cookie cookie = new Cookie("__Secure-Fgp", fingerprint);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
5.1.3 弱密钥攻击
问题: 使用弱密钥,攻击者可以暴力破解。
防护:
// ❌ 错误示例
String secret = "secret";
// ✅ 正确示例:使用足够长度的随机密钥
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 或使用至少 64 字符的随机字符串
5.2 Token 存储建议
| 存储方式 | 优点 | 缺点 | 推荐场景 |
|---------|------|------|---------|
| sessionStorage | 标签页关闭即清除 | 不能跨标签页 | 高安全要求 |
| localStorage | 持久化存储 | XSS 风险 | 一般应用 |
| HttpOnly Cookie | 防 XSS | 需防 CSRF | 传统 Web 应用 |
| 内存(闭包) | 最安全 | 刷新丢失 | SPA 应用 |
5.3 前端存储示例
// 使用 sessionStorage 存储
function login(username, password) {
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
sessionStorage.setItem('token', data.token);
});
}
// 请求时携带 Token
function fetchWithAuth(url, options = {}) {
const token = sessionStorage.getItem('token');
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}
5.4 Token 刷新机制
@PostMapping("/refresh")
public ResponseEntity<LoginResponse> refreshToken(
@RequestHeader("Authorization") String authHeader) {
String oldToken = authHeader.substring(7);
String username = tokenManager.getUsernameFromToken(oldToken);
// 验证旧 Token(可以允许已过期但未超过刷新窗口的 Token)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 生成新 Token
String newToken = tokenManager.generateToken(userDetails);
return ResponseEntity.ok(new LoginResponse(newToken));
}
5.5 Token 黑名单(登出实现)
@Service
public class TokenBlacklistService {
// 使用 Redis 存储黑名单
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void blacklistToken(String token) {
// 获取 Token 剩余有效时间
Date expiration = tokenManager.getExpirationFromToken(token);
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
// 将 Token 加入黑名单,过期时间与 Token 一致
redisTemplate.opsForValue().set(
"blacklist:" + token,
"1",
ttl,
TimeUnit.MILLISECONDS
);
}
}
public boolean isBlacklisted(String token) {
return redisTemplate.hasKey("blacklist:" + token);
}
}
附录
A. 常用签名算法
| 算法 | 类型 | 密钥长度 | 说明 |
|-----|------|---------|------|
| HS256 | 对称 | 256 位 | HMAC + SHA-256 |
| HS384 | 对称 | 384 位 | HMAC + SHA-384 |
| HS512 | 对称 | 512 位 | HMAC + SHA-512 |
| RS256 | 非对称 | 2048+ 位 | RSA + SHA-256 |
| ES256 | 非对称 | 256 位 | ECDSA + SHA-256 |
B. 参考资源
📝 提示: 生产环境中务必使用 HTTPS,并定期轮换密钥。