1. 基本概念解析
1.1 JWT (JSON Web Token)
JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这些信息可以被验证和信任,因为它是数字签名的。JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JWT的结构由三部分组成,以点(.)分隔:
- Header(头部):指定签名算法和令牌类型
- Payload(负载):包含声明(claims)
- Signature(签名):用于验证消息未被篡改
1.1.1 JWT规范详解
JWT规范(RFC 7519)定义了以下核心内容:
- 令牌格式:JWT采用紧凑的、URL安全的字符串格式,便于在HTTP头部或URL参数中传输
- 声明(Claims)结构:定义了标准的声明名称和格式
- 签名和加密:规定了JWT可以通过JWS(JSON Web Signature)进行签名或通过JWE(JSON Web Encryption)进行加密
- 处理规则:规定了如何创建和验证JWT的规则
1.2 JJWT (Java JWT)
JJWT是一个Java库,提供了端到端的JWT创建和验证功能。它是为Java开发者设计的,使JWT规范的实现变得简单直观。JJWT的目标是易于使用和理解,同时提供必要的安全功能。
1.2.1 JJWT实现的JWT规范
JJWT实现了JWT规范的以下部分:
- JWT创建和解析:提供了流畅的API来创建和解析JWT
- JWS支持:支持使用各种算法(如HMAC、RSA、ECDSA)对JWT进行签名
- JWE支持:完整实现了JSON Web Encryption,用于创建和解析加密的JWT
- JWK支持:实现了JSON Web Key规范,用于表示和管理加密密钥
- 声明处理:支持所有标准声明和自定义声明
- 异常处理:提供了详细的异常类型,用于处理各种JWT验证错误
- 压缩支持:支持对JWT进行压缩,减小令牌大小
- Edwards曲线算法支持:支持EdDSA等现代加密算法
JJWT是一个完整的JOSE(JavaScript Object Signing and Encryption)规范实现,包括JWT、JWS、JWE、JWK等所有核心组件。
1.3 JWTS
JWTS是JJWT库中的一个类,提供了静态方法来创建和解析JWT。在JJWT库中,Jwts类是一个工厂类,提供了流畅的API来创建和验证JWT。
1.4 JWT vs JWTs
"JWT"和"JWTs"本质上指的是同一个概念,只是表达方式略有不同:
- JWT (JSON Web Token):指的是单个令牌,强调的是技术概念
- JWTs (JSON Web Tokens):指的是多个JWT令牌,通常在讨论JWT的集合或应用场景时使用
这两个术语在技术文档中经常互换使用,没有实质性的区别。
1.5 Payload与Claims
Payload是JWT的第二部分,包含了声明(Claims)。声明是关于实体(通常是用户)和其他数据的声明。
当Payload是JSON对象时,通常称为Claims。Claims是JWT中包含的键值对,用于传递信息。
1.6 Claim
Claim是JWT中的单个键值对,表示一个声明。例如,{"name": "John Doe"}中,"name"是键,"John Doe"是值,整体构成一个claim。
1.7 Registered Claims(注册声明)
JWT规范预定义了一组标准声明,这些声明不是强制的,但推荐使用:
- Subject (sub):令牌的主题,通常是用户ID
- Issuer (iss):令牌的发行者
- Audience (aud):令牌的接收者
- Expiration (exp):令牌的过期时间
- Not Before (nbf):令牌生效的开始时间
- Issued At (iat):令牌的发行时间
- JWT ID (jti):令牌的唯一标识符
在JJWT中,不建议使用通用的claim方法来设置这些标准声明,而是使用专门的方法(如subject()、issuer()等),这样可以提高代码的可读性。
1.8 Public Claims(公共声明)
Public Claims是可以由使用JWT的开发者自由定义的声明,但为了避免冲突,应该在IANA JSON Web Token Registry中注册或使用包含抗冲突命名空间的URI来定义。
例如:
{
"https://example.com/claims/role": "admin"
}
这种方式确保了不同应用程序之间的声明名称不会冲突。
1.9 Private Claims(私有声明)
Private Claims是使用JWT的各方之间共同约定的声明,既不是注册声明也不是公共声明。这些声明用于在特定应用程序上下文中共享信息。
例如:
{
"username": "john_doe",
"employee_id": "E12345"
}
值得注意的是,subject、issuedAt等标准声明与自定义声明(如claim("name", "John Doe"))在内部都会合并到同一个Claims对象中。在下面的示例中:
String jwtToken = Jwts.builder()
.subject("user123")
.claim("name", "John Doe")
.claim("admin", true)
.issuedAt(Instant.now())
.expiration(Instant.now().plus(1, ChronoUnit.HOURS))
.signWith(key)
.compact();
subject、自定义claims和issuedAt都会被合并到同一个Claims对象中,构成JWT的payload部分。
1.10 JWT的安全级别:JWS和JWE
JWT有三种安全级别:
-
Unsecured JWT:不包含任何加密信息的JWT,仅由Header和Payload两部分组成,Header中的
alg值为none。这种JWT不提供任何安全保障,不应在生产环境中使用。 -
JWS (JSON Web Signature):带签名的JWT,由Header、Payload和Signature三部分组成。JWS确保数据在传输过程中没有被篡改(完整性),但不会加密数据本身,Payload中的信息对任何人都是可见的。这是最常用的JWT形式。
-
JWE (JSON Web Encryption):加密的JWT,用于传输敏感信息。JWE对Payload进行加密,确保只有持有正确密钥的接收方才能查看内容(机密性)。当JWT需要传输敏感信息(如社会安全号码、银行账号等)时,应使用JWE。
在大多数应用场景中,JWS已经足够满足需求,因为JWT通常不用于传输高度敏感的信息,而是用于身份验证和授权。
2. 签名密钥的用法
2.1 在JJWT中创建签名密钥的方式
2.1.1 手动根据密钥字符串创建
SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
这种方法需要开发者提供一个密钥字符串,然后将其转换为适合HMAC-SHA算法的SecretKey。这是一种常见的做法,特别是在Spring Boot应用中,密钥通常存储在配置文件中。
2.1.2 自动生成密钥
SecretKey key = Jwts.SIG.HS256.key().build();
这种方法会自动生成一个适合HS256算法的随机密钥。这对于测试或者一次性使用很方便,但在生产环境中,你需要保存这个密钥以便后续验证令牌。
2.2 自动生成密钥的保存策略
对于自动生成的密钥(如使用Jwts.SIG.HS256.key().build()或RSA密钥对),有几种保存策略:
2.2.1 密钥持久化
可以将生成的密钥序列化并保存到文件系统或数据库中:
// 生成密钥
SecretKey key = Jwts.SIG.HS256.key().build();
// 将密钥编码为Base64字符串
String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded());
// 保存encodedKey到配置文件、数据库或安全的密钥存储
// ...
// 后续使用时,从存储中读取并还原
Byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
SecretKey restoredKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256");
2.2.2 RSA密钥对的保存
对于非对称加密(如RSA),需要保存公钥和私钥:
// 生成RSA密钥对
KeyPair keyPair = Jwts.SIG.RS256.keyPair().build();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 保存公钥和私钥
// ...
3. JJWT的重要概念与用法
3.1 JWT的创建
JJWT提供了流畅的API来创建JWT:
String jwtToken = Jwts.builder()
.subject("user123") // 设置主题(通常是用户ID)
.claim("name", "John Doe") // 添加自定义声明
.claim("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN")) // 添加复杂类型
.issuer("myapp") // 设置发行者
.issuedAt(Instant.now()) // 设置发行时间
.expiration(Instant.now().plus(1, ChronoUnit.HOURS)) // 设置过期时间
.signWith(key) // 使用密钥签名
.compact(); // 生成紧凑的JWT字符串
3.2 JWT的解析与验证
Claims claims = Jwts.parser()
.verifyWith(key) // 指定用于验证签名的密钥
.build() // 构建解析器
.parseSignedClaims(token) // 解析并验证JWT
.getPayload(); // 获取Claims
// 获取特定声明
String subject = claims.getSubject();
String name = claims.get("name", String.class);
List<String> roles = claims.get("roles", List.class);
3.3 异常处理
JJWT在解析和验证JWT时可能抛出多种异常,应当适当处理:
try {
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
// 处理有效的令牌
} catch (ExpiredJwtException e) {
// 处理过期的令牌
} catch (UnsupportedJwtException e) {
// 处理不支持的JWT
} catch (MalformedJwtException e) {
// 处理格式错误的JWT
} catch (SignatureException e) {
// 处理签名验证失败
} catch (IllegalArgumentException e) {
// 处理非法参数
}
3.4 压缩
JJWT支持对JWT进行压缩,减小令牌大小:
String compressedJwt = Jwts.builder()
.subject("user123")
// ... 其他声明 ...
.compressWith(CompressionAlgorithms.DEFLATE) // 使用DEFLATE算法压缩
.signWith(key)
.compact();
4. 在Spring Boot中的实际应用
4.1 JWT配置类
@Component
@ConfigurationProperties(prefix = "jwt")
@Getter
@Setter
public class JwtConfig {
private String secret;
private long accessTokenExpiration;
private long refreshTokenExpiration;
private String tokenPrefix;
private String headerString;
private String issuer;
// 可选:用于存储自动生成的密钥的Base64编码字符串
private String generatedKeyBase64;
}
4.2 基于密钥字符串的JWT工具类
以下是一个使用手动指定密钥的JwtTokenUtil类,它是最常见的实现方式:
@Component
public class JwtTokenUtil {
// 初始化密钥
@PostConstruct
public void init() {
// 从配置中加载密钥
secretKey = Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8));
}
// 生成访问令牌
public String generateAccessToken(UserDetails userDetails) {
return generateToken(userDetails.getUsername(), jwtConfig.getAccessTokenExpiration());
}
// 验证令牌
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
4.3 使用自动生成密钥的JWT工具类
以下是一个使用自动生成密钥的JwtTokenUtil类,它演示了如何优雅地保存和获取生成的密钥:
@Component
public class AutoGenKeyJwtTokenUtil {
private static final Logger logger = LoggerFactory.getLogger(AutoGenKeyJwtTokenUtil.class);
@Autowired
private JwtConfig jwtConfig;
@Autowired
private Environment environment;
// 用于存储生成的密钥
private SecretKey secretKey;
// 应用启动时初始化密钥
@PostConstruct
public void init() {
// 尝试从配置或文件加载密钥,如果不存在则生成新密钥
secretKey = loadOrGenerateKey();
}
/**
* 加载或生成密钥
* 优先级:1. 配置文件中的generatedKeyBase64 2. 密钥文件 3. 新生成的密钥
*/
private SecretKey loadOrGenerateKey() {
// 1. 尝试从配置文件加载Base64编码的密钥
if (StringUtils.hasText(jwtConfig.getGeneratedKeyBase64())) {
try {
byte[] decodedKey = Base64.getDecoder().decode(jwtConfig.getGeneratedKeyBase64());
return new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256");
} catch (Exception e) {
logger.warn("无法从配置文件加载密钥,将尝试其他方式", e);
}
}
// 2. 尝试从文件加载密钥
SecretKey keyFromFile = loadKeyFromFile();
if (keyFromFile != null) {
return keyFromFile;
}
// 3. 生成新密钥并保存
SecretKey newKey = Jwts.SIG.HS256.key().build();
saveKeyToFile(newKey);
// 4. 输出密钥的Base64编码,以便可以添加到配置文件中
String encodedKey = Base64.getEncoder().encodeToString(newKey.getEncoded());
logger.info("已生成新的JWT密钥,建议将其添加到配置文件中:jwt.generated-key-base64={}", encodedKey);
return newKey;
}
/**
* 从文件加载密钥
*/
private SecretKey loadKeyFromFile() {
File keyFile = getKeyFile();
if (!keyFile.exists()) {
return null;
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(keyFile))) {
Object object = ois.readObject();
if (object instanceof SecretKey) {
return (SecretKey) object;
}
} catch (Exception e) {
logger.warn("从文件加载密钥失败", e);
}
return null;
}
/**
* 将密钥保存到文件
*/
private void saveKeyToFile(SecretKey key) {
File keyFile = getKeyFile();
try {
// 确保目录存在
keyFile.getParentFile().mkdirs();
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(keyFile))) {
oos.writeObject(key);
}
// 设置文件权限(仅在非Windows系统上)
if (!System.getProperty("os.name").toLowerCase().contains("win")) {
Files.setPosixFilePermissions(keyFile.toPath(),
PosixFilePermissions.fromString("rw-------"));
}
} catch (Exception e) {
logger.error("保存密钥到文件失败", e);
}
}
/**
* 获取密钥文件对象,根据不同环境使用不同的文件名
*/
private File getKeyFile() {
String activeProfile = environment.getActiveProfiles().length > 0 ?
environment.getActiveProfiles()[0] : "default";
String fileName = "jwt-key-" + activeProfile + ".dat";
// 在生产环境中,应该使用更安全的位置存储密钥
String dataDir = System.getProperty("user.home") + File.separator + ".myapp" + File.separator + "data";
return new File(dataDir, fileName);
}
/**
* 生成访问令牌
*/
public String generateAccessToken(UserDetails userDetails) {
return generateToken(userDetails.getUsername(), jwtConfig.getAccessTokenExpiration());
}
/**
* 生成令牌
*/
private String generateToken(String subject, long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.subject(subject)
.issuedAt(now)
.issuer(jwtConfig.getIssuer())
.expiration(expiryDate)
.id(UUID.randomUUID().toString())
.signWith(secretKey)
.compact();
}
/**
* 从令牌中获取所有声明
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 验证令牌
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
4.4 配置文件示例
# application-jwt.yml
jwt:
# 可选:手动指定的密钥(如果不使用自动生成的密钥)
secret: secret_key_1234567890_abcdefg_123456_parade
# 可选:自动生成的密钥的Base64编码(优先级高于secret)
generated-key-base64: HhOtfHLz98ftVB0Oi/zgS8ZpRjZV2d9w+ld42Qmbnao=
# 访问令牌过期时间(毫秒),默认30分钟
access-token-expiration: 1800000
# 刷新令牌过期时间(毫秒),默认7天
refresh-token-expiration: 604800000
# 令牌前缀
token-prefix: "Bearer "
# 请求头名称
header-string: "Authorization"
# 令牌发行者
issuer: myapp