第一次接触JJWT时,我被它简洁的API设计惊艳到了。作为Java开发者,我们经常需要处理用户认证问题,而JJWT让生成和验证JWT变得像搭积木一样简单。先来看个最基础的例子:
java复制// 生成密钥
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 构建JWT
String token = Jwts.builder()
.setSubject("user123") // 设置用户标识
.setIssuedAt(new Date()) // 签发时间
.signWith(key) // 签名
.compact();
这段代码生成的token包含三部分:头部(Header)、载荷(Payload)和签名(Signature),用点号分隔。比如你可能得到这样的字符串:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaWF0IjoxNjUwMjM4MDIyfQ.s5D5zX2H2J7ZVlCx7jJ7kZxY5X5ZxY5X5ZxY5X5ZxY5X5
密钥生成是第一个需要注意的点。上面的Keys.secretKeyFor()会帮我们生成足够安全的密钥,但实际项目中更常见的做法是从配置读取密钥字符串:
java复制String secretStr = "mySuperSecretKey123!@#";
SecretKey key = Keys.hmacShaKeyFor(secretStr.getBytes(StandardCharsets.UTF_8));
验证令牌同样简单:
java复制Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
System.out.println("用户ID: " + claims.getSubject());
这里容易踩的坑是忘记处理异常。JWT验证可能抛出多种异常,建议用try-catch包裹:
java复制try {
// 验证代码...
} catch (ExpiredJwtException e) {
System.out.println("令牌已过期");
} catch (JwtException e) {
System.out.println("无效令牌");
}
JWT的载荷部分可以包含各种声明(Claims),这些声明分为三类:
最常用的注册声明包括:
java复制Date now = new Date();
Date exp = new Date(now.getTime() + 3600_000); // 1小时后过期
String token = Jwts.builder()
.setIssuer("myApp") // 签发者
.setSubject("user123") // 主题
.setAudience("client1") // 接收方
.setExpiration(exp) // 过期时间
.setNotBefore(now) // 生效时间
.setIssuedAt(now) // 签发时间
.setId(UUID.randomUUID().toString()) // JWT ID
.claim("role", "admin") // 自定义声明
.signWith(key)
.compact();
时间处理是个需要特别注意的点。我遇到过时区问题导致的验证失败,建议:
获取声明值时,JJWT提供了类型安全的方法:
java复制Claims claims = // 解析得到的claims对象
String issuer = claims.getIssuer();
Date exp = claims.getExpiration();
String role = claims.get("role", String.class); // 类型转换
对于数组类型的声明,可以这样处理:
java复制.claim("scopes", Arrays.asList("read", "write"))
// 获取时
List<String> scopes = claims.get("scopes", List.class);
基础的JWS(签名令牌)只能防篡改,不能防窥探。如果载荷中包含敏感信息,就该使用JWE(加密令牌)了。
JWS与JWE的核心区别:
| 特性 | JWS | JWE |
|---|---|---|
| 数据可见性 | 载荷明文 | 载荷加密 |
| 保护重点 | 防篡改 | 防篡改+防窥探 |
| 性能开销 | 低 | 较高 |
| 适用场景 | 不含敏感信息的令牌 | 含敏感信息的令牌 |
生成JWE需要先准备加密密钥:
java复制// 生成RSA密钥对
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
// 构建JWE
String jwe = Jwts.builder()
.setSubject("user123")
.encryptWith(keyPair.getPublic(), KeyEncryptionAlgorithm.RSA_OAEP, ContentEncryptionAlgorithm.A256GCM)
.compact();
解析JWE需要使用私钥:
java复制Jws<Claims> jws = Jwts.parserBuilder()
.decryptWith(keyPair.getPrivate())
.build()
.parseClaimsJws(jwe);
密钥管理是安全的核心。我推荐的做法:
对于性能敏感的场景,可以考虑:
java复制// 启用压缩(仅建议用于JWE)
String jwe = Jwts.builder()
.compressWith(CompressionCodecs.DEFLATE)
// 其他配置...
.compact();
在微服务架构中,JWT通常作为无状态认证方案。下面分享几个实战中总结的经验:
令牌刷新方案:
java复制// 生成访问令牌(短有效期)
String accessToken = Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + 30_000)) // 30秒
// 其他配置...
.compact();
// 生成刷新令牌(长有效期)
String refreshToken = Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + 30_000_000)) // 30天
.claim("token_type", "refresh")
// 其他配置...
.compact();
多租户场景下的密钥管理:
java复制// 租户特定的密钥解析器
SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
public Key resolveSigningKey(JwsHeader header, Claims claims) {
String tenantId = claims.get("tenant_id", String.class);
return getTenantKey(tenantId); // 根据租户ID获取对应密钥
}
};
Jwts.parserBuilder()
.setSigningKeyResolver(signingKeyResolver)
.build()
.parseClaimsJws(token);
性能优化技巧:
黑名单处理:
虽然JWT本身是无状态的,但某些场景下需要实现令牌失效:
java复制// 在Redis中维护黑名单
String tokenId = claims.getId();
redisTemplate.opsForValue().set("blacklist:"+tokenId, "1", Duration.ofMinutes(30));
// 验证时检查黑名单
if (redisTemplate.hasKey("blacklist:"+claims.getId())) {
throw new JwtException("令牌已失效");
}
在Spring Security集成时,可以这样配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()));
}
}