Java JWT权威指南:原理实战与案例
这是一份非常详细、实用、通俗易懂、权威且全面的 Java JWT 指南,涵盖了其方方面面,包含原理解释、最佳实战代码和可直接运行的完整系统案例。
Java JWT 全面指南:原理、实战与案例
目录
- 理解 JWT:基础概念 1.1 什么是 JWT? 1.2 为什么需要 JWT? 1.3 JWT 的核心优势 1.4 JWT 的典型应用场景
- JWT 的结构剖析 2.1 Header (头部) 2.2 Payload (载荷/声明) 2.3 Signature (签名) 2.4 组合与传输:JWT 字符串
- Java 中的 JWT 实现库 3.1 主流库介绍与选择 (Java-JWT, jose4j, Nimbus-JOSE-JWT) 3.2 重点推荐:
java-jwt(auth0) - 实战:创建和验证 JWT 4.1 环境准备 (依赖引入) 4.2 密钥/密钥对生成与管理 4.3 创建 JWT (Token 生成) 4.4 解析与验证 JWT (Token 验证) 4.5 处理过期 (
exp) 和其他声明 - 最佳实践与进阶技巧 5.1 密钥安全:存储与轮换 5.2 令牌刷新机制 5.3 选择合适的签名算法 (HS256, RS256, ES256) 5.4 自定义声明 (Claims) 5.5 处理令牌失效与异常 5.6 安全注意事项 (防止 XSS, CSRF, 令牌盗用)
- 完整案例一:基于 JWT 的 Web 应用 API 保护 (Spring Boot) 6.1 项目初始化与依赖 6.2 用户登录与 JWT 签发 6.3 JWT 验证过滤器实现 6.4 受保护 API 端点示例 6.5 运行与测试
- 完整案例二:微服务间 JWT 认证 7.1 场景描述 (服务 A -> 服务 B) 7.2 服务 A:生成包含特定声明的 JWT 7.3 服务 B:验证 JWT 并提取声明授权 7.4 代码实现与交互流程
- 总结与资源
1. 理解 JWT:基础概念
1.1 什么是 JWT?
JWT (JSON Web Token) 是一个开放标准 (RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。这些信息可以被验证和信任,因为它是经过数字签名的。
通俗地说,JWT 就像一张加密的身份证或通行证。服务器在用户登录成功后,生成这张“身份证”发给用户(通常是客户端,如浏览器或移动 App)。之后,用户每次向服务器请求需要认证的资源时,都需要出示这张“身份证”。服务器通过验证这张“身份证”的真伪(签名)和有效性(如是否过期),就能确认用户的身份和权限,而无需再次查询数据库或会话存储。
1.2 为什么需要 JWT?
在传统 Web 应用中,通常使用 Session-Cookie 机制来管理用户状态:
- 用户登录,服务器创建 Session 并存储(通常在内存或数据库),将 Session ID 通过 Cookie 返回给浏览器。
- 后续请求,浏览器自动带上 Cookie (包含 Session ID),服务器根据 ID 查找 Session 验证身份。
这种方式存在一些痛点:
- 扩展性:当应用需要分布式部署或使用多台服务器时,Session 需要共享(如使用 Redis),增加了复杂性和维护成本。
- 跨域问题 (CORS):在前后端分离或跨域 API 调用时,Cookie 的处理可能更复杂。
- 移动端适配:原生 App 对 Cookie 的支持不如浏览器原生。
- 无状态性:服务器端需要维护 Session 状态。
JWT 旨在解决这些问题:
- 无状态/自包含:JWT 本身包含了所有需要的信息(用户标识、权限、有效期等),服务器无需存储会话状态。验证只需根据 JWT 本身和密钥进行。
- 跨域友好:JWT 通常通过 HTTP Header (如
Authorization: Bearer) 传输,天然支持跨域。 - 移动端友好:易于在移动 App 中存储和传输。
- 标准化:基于开放标准,语言无关,易于在不同系统间交换。
1.3 JWT 的核心优势
- 无状态性 (Stateless):服务器不需要存储会话信息,简化架构,易于水平扩展。
- 跨域支持 (CORS):易于在单页应用 (SPA)、原生移动应用和跨域 API 中使用。
- 信息自包含:Token 内可包含用户标识、角色、权限等必要信息,减少数据库查询。
- 安全性:通过数字签名保证 Token 的完整性和来源可信,防止篡改。
- 标准化与通用性:遵循 RFC 7519 标准,各种编程语言都有成熟库支持。
1.4 JWT 的典型应用场景
- 用户认证 (Authentication):最常见的场景。用户登录后获取 JWT,后续请求携带 JWT 访问受保护资源。
- 授权 (Authorization):JWT 的 Payload 中可以包含用户的角色 (
role) 或权限 (scope) 信息,服务器据此决定用户是否有权执行操作。 - 信息交换 (Information Exchange):安全地在各方之间传递经过签名和(可选)加密的信息。
- 单点登录 (SSO):在多个相关但独立的系统间实现一次登录,通行所有系统。JWT 可作为凭证在系统间传递。
- API 认证:保护 RESTful API 或 GraphQL 端点。
- 微服务间认证:服务 A 调用服务 B 时,携带一个由可信授权服务器签发的 JWT 证明其身份和权限。
2. JWT 的结构剖析
一个 JWT 由三部分组成,用点 (.) 分隔:
Header.Payload.Signature
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2.1 Header (头部)
头部通常由两部分组成:
typ(Type):令牌类型,这里固定为JWT。alg(Algorithm):签名算法,如HS256(HMAC SHA-256),RS256(RSA SHA-256),ES256(ECDSA SHA-256) 等。它告诉服务器使用哪种算法来验证签名。
Header 是一个 JSON 对象,例如:
{
"alg": "HS256",
"typ": "JWT"
}
这个 JSON 对象会被 Base64Url 编码,形成 JWT 的第一部分。
2.2 Payload (载荷/声明)
Payload 包含关于实体(通常是用户)的声明和其他元数据。声明有三种类型:
- 注册声明 (Registered Claims):预定义的、建议但不是强制的声明。常用有:
iss(Issuer):签发者。sub(Subject):主题,通常是用户 ID。aud(Audience):受众,接收该 JWT 的一方。exp(Expiration Time):过期时间戳 (Unix time)。非常重要!nbf(Not Before):生效时间戳。iat(Issued At):签发时间戳。jti(JWT ID):唯一标识符,防止重放攻击。
- 公共声明 (Public Claims):可以自定义的名称,但应在 IANA JSON Web Token Registry 中定义或使用防冲突命名空间(如以域名开头)。
- 私有声明 (Private Claims):自定义的声明,用于在同意使用它们的各方之间共享信息。
一个 Payload 示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516239022 + 3600 // 例如,签发后一小时过期
}
这个 JSON 对象也会被 Base64Url 编码,形成 JWT 的第二部分。
注意:Payload 只是进行了 Base64Url 编码,并未加密!任何人都可以解码查看其内容(这就是为什么它叫 “载荷” 而不是 “主体”)。因此,不要在 Payload 中放置敏感信息(如密码)。如果需要保密,应使用 JWE (JSON Web Encryption) 对 JWT 进行加密。
2.3 Signature (签名)
签名部分用于验证消息在传输过程中没有被篡改,并且(对于使用私钥签名的令牌)验证发送者是否可信。
生成签名需要:
- 将编码后的 Header 和编码后的 Payload 用点 (
.) 连接起来:base64UrlEncode(header) + "." + base64UrlEncode(payload)。 - 使用 Header 中指定的签名算法 (
alg),以及一个密钥(对于HS256是共享密钥;对于RS256是私钥),对上一步生成的字符串进行签名。 - 对签名结果进行 Base64Url 编码。
例如,对于 HS256 (HMAC SHA-256): $$ signature = HMAC_{SHA256}(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) $$
签名是 JWT 最关键的部分。接收方在验证 JWT 时,会使用相同的算法和密钥(或公钥,对于非对称算法)重新计算签名,并与收到的 Signature 进行比较。如果匹配,说明 Token 未被篡改且来源可信。
2.4 组合与传输:JWT 字符串
最终,将 Base64Url 编码后的 Header、Payload 和 Signature 用两个点 (.) 连接起来,就构成了完整的 JWT: $$ JWT = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + base64UrlEncode(signature) $$
在 HTTP 请求中传输时,通常放在 Authorization 请求头中,使用 Bearer 模式:
Authorization: Bearer
3. Java 中的 JWT 实现库
Java 生态中有多个成熟的 JWT 库。选择时需考虑活跃度、功能完整性、易用性和社区支持。
3.1 主流库介绍
java-jwt(auth0):- 由 Auth0 公司维护。
- 功能全面,支持创建、解析、验证 JWT。
- 支持多种签名算法 (
HS256,RS256,ES256等)。 - 支持自定义声明。
- 易于集成到 Spring Boot 等框架。
- 推荐理由:文档清晰,API 直观,维护活跃,社区广泛。
jjwt(Java JWT by Stormpath, 后移交至 Okta):- 早期流行库,现在由 Okta 维护。
- 同样功能丰富,API 设计略有不同。
nimbus-jose-jwt:- 功能非常强大,不仅支持 JWT (JWS),还支持 JWE (加密)、JWA、JWK 等全套 JOSE 规范。
- 适合需要高级功能或完整 JOSE 支持的项目。
- 学习曲线可能稍陡。
jose4j(Bitbucket):- 另一个功能强大的 JOSE 库,支持 JWT、JWE、JWK 等。
- 稳定且功能完备。
3.2 重点推荐:java-jwt (auth0)
本指南后续的代码示例将主要使用 java-jwt 库,因其易用性和流行度。可以通过 Maven 或 Gradle 引入:
Maven:
com.auth0
java-jwt
4.4.0
Gradle:
implementation 'com.auth0:java-jwt:4.4.0' // 请检查并使用最新版本
4. 实战:创建和验证 JWT
4.1 环境准备 (依赖引入)
确保项目中已添加 java-jwt 依赖(如上所示)。
4.2 密钥/密钥对生成与管理
对称签名 (如 HS256):
- 使用一个共享的、高强度的密钥字符串(Secret)。这个密钥必须保密!长度建议至少 32 字节 (256位)。
- 生成示例:
String secret = "your-very-long-and-secure-secret-key-at-least-32-characters"; // 实践中应从安全配置源获取
非对称签名 (如 RS256):
- 使用一对密钥:私钥用于签名,公钥用于验证。
- 生成密钥对:
import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; public KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); // 密钥大小,推荐 2048 或以上 return keyPairGenerator.generateKeyPair(); } - 安全存储:私钥必须严格保密(使用密钥库、环境变量、安全配置服务器)。公钥可以公开分发给需要验证 Token 的服务。
4.3 创建 JWT (Token 生成)
使用 java-jwt 创建 JWT 非常直观。
示例:使用 HS256 创建 JWT
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
public class JwtCreator {
public static String createJwtToken(String userId, String secret) {
// 定义过期时间 (例如,1小时后过期)
Date expiresAt = new Date(System.currentTimeMillis() + 3600000); // 3600000 ms = 1 hour
// 创建并返回 JWT Token
return JWT.create()
.withSubject(userId) // 主题 (通常是用户ID)
.withIssuer("your-issuer-name") // 签发者
.withIssuedAt(new Date()) // 签发时间
.withExpiresAt(expiresAt) // 过期时间
.withClaim("name", "John Doe") // 自定义声明
.withClaim("role", "admin") // 自定义声明
.sign(Algorithm.HMAC256(secret)); // 使用 HS256 算法和密钥签名
}
public static void main(String[] args) {
String secret = "your-very-long-and-secure-secret-key";
String userId = "123456";
String jwtToken = createJwtToken(userId, secret);
System.out.println("Generated JWT Token:");
System.out.println(jwtToken);
}
}
示例:使用 RS256 创建 JWT
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.util.Date;
public class JwtCreatorRsa {
public static String createJwtTokenRsa(String userId, RSAPrivateKey privateKey) {
Date expiresAt = new Date(System.currentTimeMillis() + 3600000);
return JWT.create()
.withSubject(userId)
.withIssuer("your-issuer-name")
.withIssuedAt(new Date())
.withExpiresAt(expiresAt)
.sign(Algorithm.RSA256(null, privateKey)); // 使用 RSA256 和私钥签名 (公钥设为null,仅用于签名)
}
public static void main(String[] args) throws NoSuchAlgorithmException {
// 生成密钥对 (实际应用中私钥应安全存储)
KeyPair keyPair = generateKeyPair(); // 使用前面定义的 generateKeyPair 方法
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
String userId = "123456";
String jwtToken = createJwtTokenRsa(userId, privateKey);
System.out.println("Generated JWT Token (RS256):");
System.out.println(jwtToken);
}
}
4.4 解析与验证 JWT (Token 验证)
验证 JWT 包括:
- 解析 Token 结构 (Header, Payload)。
- 验证签名是否正确(防止篡改)。
- 验证注册声明(特别是
exp过期时间)。 - (可选)验证其他声明(如
iss,aud)。
示例:验证 HS256 JWT
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JwtValidator {
public static DecodedJWT validateJwtToken(String jwtToken, String secret) throws JWTVerificationException {
// 创建验证器,指定算法和密钥
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("your-issuer-name") // 验证签发者
.build(); // 可添加更多验证条件,如 .withSubject(...), .withAudience(...)
// 验证 Token: 签名 + 声明 (如 exp)
return verifier.verify(jwtToken); // 如果验证失败,抛出 JWTVerificationException
}
public static void main(String[] args) {
String secret = "your-very-long-and-secure-secret-key";
String jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 替换为实际Token
try {
DecodedJWT decodedJWT = validateJwtToken(jwtToken, secret);
System.out.println("Token is valid!");
System.out.println("Subject (User ID): " + decodedJWT.getSubject());
System.out.println("Expires at: " + decodedJWT.getExpiresAt());
System.out.println("Custom Claim 'role': " + decodedJWT.getClaim("role").asString());
} catch (JWTVerificationException e) {
System.err.println("Token verification failed: " + e.getMessage());
}
}
}
示例:验证 RS256 JWT
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.security.interfaces.RSAPublicKey;
public class JwtValidatorRsa {
public static DecodedJWT validateJwtTokenRsa(String jwtToken, RSAPublicKey publicKey) throws JWTVerificationException {
Algorithm algorithm = Algorithm.RSA256(publicKey, null); // 使用 RSA256 和公钥验证
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("your-issuer-name")
.build();
return verifier.verify(jwtToken);
}
public static void main(String[] args) {
// 假设 publicKey 已从安全来源获取
RSAPublicKey publicKey = ...; // 获取公钥
String jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 替换为实际Token
try {
DecodedJWT decodedJWT = validateJwtTokenRsa(jwtToken, publicKey);
System.out.println("Token is valid (RS256)!");
// ... 提取信息
} catch (JWTVerificationException e) {
System.err.println("Token verification failed: " + e.getMessage());
}
}
}
4.5 处理过期 (exp) 和其他声明
库在调用 verifier.verify(token) 时会自动检查 exp 和 nbf 声明(如果存在)。如果 Token 已过期或尚未生效,将抛出 TokenExpiredException 或 InvalidClaimException。
你可以在 JWTVerifier 构建器中添加额外的声明验证:
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("your-issuer-name")
.withAudience("your-audience") // 验证受众
.withClaim("role", "admin") // 验证自定义声明存在且值匹配
.build();
使用 DecodedJWT 对象可以方便地获取声明值:
String subject = decodedJWT.getSubject(); // 获取标准声明
String role = decodedJWT.getClaim("role").asString(); // 获取自定义声明
Date expiresAt = decodedJWT.getExpiresAt();
Map allClaims = decodedJWT.getClaims(); // 获取所有声明
5. 最佳实践与进阶技巧
5.1 密钥安全:存储与轮换
- 对称密钥 (HS256):
- 使用足够长的随机字符串(至少 32 字节)。
- 绝不能硬编码在代码中!
- 使用安全的配置源:环境变量、专用配置服务器(如 Spring Cloud Config)、密钥管理系统(KMS)、或受保护的配置文件(在部署时注入)。
- 非对称密钥 (RS256, ES256):
- 私钥的安全级别要求更高。优先使用密钥管理系统 (KMS) 或硬件安全模块 (HSM)。
- 公钥可以安全地分发给验证方(例如,通过 JWK Set 端点)。
- 密钥轮换:
- 定期更换密钥以降低泄露风险。
- 设计时考虑支持多密钥版本(例如,在验证时尝试用新、旧两个密钥验证)。
- 轮换期间,新 Token 使用新密钥签发,旧 Token 在过期前仍可用旧密钥验证。
5.2 令牌刷新机制
JWT 本身是无状态的,过期时间 (exp) 是固定的。实现类似 Session 的持续活跃需要刷新机制:
- 颁发两个 Token:
- Access Token:有效期较短(如 15-30 分钟),用于访问资源。
- Refresh Token:有效期较长(如几天或几周),存储在安全的持久化存储(如数据库),仅用于获取新的 Access Token。
- 当 Access Token 过期时,客户端使用 Refresh Token 向特定的
/refresh端点请求新的 Access Token。 - 服务器验证 Refresh Token 的有效性(检查是否被撤销或过期),然后颁发新的 Access Token。
- Refresh Token 本身也应可过期并可撤销。用户登出时,应使相关 Refresh Token 失效。
5.3 选择合适的签名算法
HS256(HMAC SHA-256):- 优点:计算速度快,实现简单。
- 缺点:对称算法,使用同一个密钥进行签名和验证。密钥必须在签发方和所有验证方之间安全共享。如果密钥泄露,攻击者可以签发任意 Token。适用于单服务或可信环境。
RS256/RS512(RSA SHA-256/512):- 优点:非对称算法,私钥签名,公钥验证。公钥可以公开分发。私钥泄露只影响签发,不影响验证。公钥泄露无风险。安全性更高。
- 缺点:RSA 计算比 HMAC 慢。
- 推荐:适用于多服务、分布式系统、公钥基础设施 (PKI) 场景。是最常用的算法。
ES256/ES512(ECDSA SHA-256/512):- 优点:非对称算法,与 RSA 类似,但使用椭圆曲线加密 (ECC)。同等安全强度下,密钥长度比 RSA 短,签名也更短。
- 缺点:支持度可能略低于 RSA,性能取决于具体实现。
- 推荐:对 Token 大小敏感的场景(如移动端)。
5.4 自定义声明 (Claims)
- 使用
.withClaim("claimName", value)方法添加自定义声明。 value可以是String,Integer,Long,Double,Boolean,Date。- 避免过大:Payload 过大会增加网络开销。
- 避免敏感信息:如前所述,Payload 是 Base64Url 编码,不是加密!
- 命名规范:使用有意义的名称。对于可能冲突的名称,建议使用命名空间(如
"https://yourdomain.com/claims/userType")。
5.5 处理令牌失效与异常
在验证时,verifier.verify(token) 可能抛出多种异常:
JWTVerificationException:通用验证失败。AlgorithmMismatchException:Token 的alg声明与验证器要求不匹配。SignatureVerificationException:签名验证失败(Token 被篡改)。TokenExpiredException:Token 已过期 (exp)。InvalidClaimException:某个声明验证失败(如iss,aud,nbf, 或自定义声明不匹配)。
应在代码中妥善捕获这些异常,并返回适当的错误响应(如 HTTP 401 Unauthorized 或 403 Forbidden)。
5.6 安全注意事项
- HTTPS:始终在 HTTPS 上传输 JWT,防止中间人攻击窃听 Token。
- 令牌存储:
- 浏览器:存储在
HttpOnly、Secure的 Cookie 中(防 XSS 读取)。或者使用localStorage/sessionStorage但要防范 XSS(需应用本身做好 XSS 防护)。 - 移动 App:使用安全的存储机制(如 Android Keystore, iOS Keychain)。
- 浏览器:存储在
- 令牌盗用:如果 Token 被盗,攻击者可以冒充用户。缩短 Token 有效期(Access Token)和使用 Refresh Token 机制可以降低风险。对于极高安全要求,可结合其他因素(如 IP 绑定)。
- 登出/令牌撤销:JWT 在有效期内始终有效。实现即时失效通常需要额外机制(如令牌黑名单、短有效期 + Refresh Token 控制)。在 Refresh Token 机制中,使 Refresh Token 失效即可阻止获取新 Access Token。
- XSS (跨站脚本攻击):如果 Token 存储在
localStorage且网站存在 XSS 漏洞,攻击者可能窃取 Token。确保应用安全无 XSS。 - CSRF (跨站请求伪造):如果使用 Cookie 存储 JWT,需防范 CSRF(例如,使用 SameSite Cookie 属性、CSRF Token)。使用
Authorization: Bearer头则不受 CSRF 影响。 - 密钥管理:再次强调,保护好你的密钥!
6. 完整案例一:基于 JWT 的 Web 应用 API 保护 (Spring Boot)
场景:实现一个简单的 Spring Boot 应用,用户通过 /login 接口使用用户名密码登录,成功则返回 JWT。后续访问 /protected 接口需在 Authorization: Bearer 头中携带有效 JWT。
6.1 项目初始化与依赖
- 创建 Spring Boot 项目 (可使用 Spring Initializr)。
- 添加依赖:
spring-boot-starter-webspring-boot-starter-security(可选,用于简化安全配置,但本示例手动处理)com.auth0:java-jwt(如前所述)
pom.xml 片段:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
com.auth0
java-jwt
4.4.0
6.2 用户登录与 JWT 签发
UserService (模拟用户存储):
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final Map userDatabase = new HashMap<>(); // 模拟数据库
public UserService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@PostConstruct
public void init() {
// 初始化一个测试用户
String username = "user";
String rawPassword = "password";
String encodedPassword = passwordEncoder.encode(rawPassword);
userDatabase.put(username, encodedPassword);
}
public boolean validateUser(String username, String password) {
String storedPassword = userDatabase.get(username);
return storedPassword != null && passwordEncoder.matches(password, storedPassword);
}
}
AuthController (处理登录):
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class AuthController {
private final UserService userService;
@Value("${jwt.secret}") // 从 application.properties 注入
private String jwtSecret;
@Value("${jwt.issuer}")
private String jwtIssuer;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
// 验证用户凭证
if (userService.validateUser(username, password)) {
// 生成 JWT
Date expiresAt = new Date(System.currentTimeMillis() + 3600000); // 1小时
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
String token = JWT.create()
.withSubject(username) // 这里用用户名做subject
.withIssuer(jwtIssuer)
.withExpiresAt(expiresAt)
.sign(algorithm);
return token; // 实际应用中,可以返回一个包含 token 的 JSON 对象
} else {
throw new RuntimeException("Invalid credentials");
}
}
}
application.properties:
jwt.secret=your-very-very-secure-hs256-secret-key # 生产环境应从安全来源获取
jwt.issuer=my-spring-boot-app
6.3 JWT 验证过滤器实现
创建一个 Filter 来拦截请求,验证 JWT。
JwtAuthorizationFilter:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final String jwtSecret;
private final String jwtIssuer;
public JwtAuthorizationFilter(String jwtSecret, String jwtIssuer) {
this.jwtSecret = jwtSecret;
this.jwtIssuer = jwtIssuer;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) {
// 没有Token,继续执行过滤器链 (后续可能被其他安全机制拦截)
chain.doFilter(request, response);
return;
}
String jwtToken = authorizationHeader.substring(BEARER_PREFIX.length());
try {
// 验证 Token
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(jwtIssuer)
.build();
DecodedJWT decodedJWT = verifier.verify(jwtToken);
// 验证成功,可以将用户信息(如 subject/username)设置在请求属性中,供后续控制器使用
request.setAttribute("username", decodedJWT.getSubject());
chain.doFilter(request, response); // 继续执行
} catch (JWTVerificationException e) {
// Token 无效
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.getWriter().write("Invalid JWT token: " + e.getMessage());
}
}
}
注册过滤器 (WebConfig):
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final String jwtSecret;
private final String jwtIssuer;
public WebConfig(@Value("${jwt.secret}") String jwtSecret, @Value("${jwt.issuer}") String jwtIssuer) {
this.jwtSecret = jwtSecret;
this.jwtIssuer = jwtIssuer;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 更常见的做法是使用 Filter,这里演示用 Interceptor 注册 Filter (Spring Boot 支持)
// 实际项目中更推荐使用 FilterRegistrationBean 注册 Servlet Filter
}
// 使用 FilterRegistrationBean 更标准 (在 Spring Boot 主类或配置类中)
@Bean
public FilterRegistrationBean jwtFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtAuthorizationFilter(jwtSecret, jwtIssuer));
registrationBean.addUrlPatterns("/protected/*"); // 只保护 /protected 下的路径
return registrationBean;
}
}
6.4 受保护 API 端点示例
ProtectedController:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class ProtectedController {
@GetMapping("/protected/hello")
public String sayHello(@RequestAttribute("username") String username) {
return "Hello, " + username + "! This is a protected resource.";
}
}
6.5 运行与测试
- 启动 Spring Boot 应用。
- 获取 Token:
- 使用 Postman 或 curl 发送 POST 请求到
http://localhost:8080/login:POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=user&password=password - 响应应包含 JWT Token。
- 使用 Postman 或 curl 发送 POST 请求到
- 访问受保护资源:
- 使用 Postman 或 curl 发送 GET 请求到
http://localhost:8080/protected/hello,并设置 Header:Authorization: Bearer - 应收到 "Hello, user! This is a protected resource." 响应。
- 使用 Postman 或 curl 发送 GET 请求到
- 测试无效 Token:
- 修改 Token 或过期后重试,应收到 401 Unauthorized 错误。
7. 完整案例二:微服务间 JWT 认证
场景:有两个服务,ServiceA 和 ServiceB。ServiceA 需要调用 ServiceB 的一个受保护接口 /serviceb/resource。ServiceA 使用一个由双方信任的 AuthService 签发的 JWT 来证明其身份和权限。
7.1 场景描述
- AuthService:负责签发 JWT。它持有私钥 (RS256)。
- ServiceA:
- 需要调用 ServiceB。
- 向 AuthService 请求一个 JWT (包含
iss,sub(ServiceA ID),aud(ServiceB), 可能还有权限声明)。 - 在调用 ServiceB 时,在
Authorization: Bearer头中携带此 JWT。
- ServiceB:
- 暴露受保护的接口
/serviceb/resource。 - 持有 AuthService 的公钥。
- 验证 JWT 的签名、
iss、aud、exp等。 - 根据 JWT 中的声明(如
sub和自定义权限)决定是否授权请求。
- 暴露受保护的接口
7.2 服务 A:生成包含特定声明的 JWT
假设 ServiceA 已经通过某种安全方式从 AuthService 获取了 JWT。这里模拟在 ServiceA 内部生成(实际中应由 AuthService 签发)。
ServiceAController (模拟获取Token并调用B):
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.security.interfaces.RSAPrivateKey; // 假设已获取
import java.util.Date;
@RestController
public class ServiceAController {
@Value("${auth.service.issuer}")
private String issuer;
@Value("${serviceb.audience}")
private String servicebAudience;
private final RestTemplate restTemplate = new RestTemplate();
// 假设 privateKey 已注入 (例如从配置)
private final RSAPrivateKey privateKey;
public ServiceAController(RSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@GetMapping("/call-serviceb")
public String callServiceB() {
// 1. 生成 JWT (模拟从AuthService获取)
String jwtToken = generateServiceToken();
// 2. 使用 RestTemplate 调用 ServiceB,携带 JWT
String servicebUrl = "http://serviceb-host:port/serviceb/resource";
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.set("Authorization", "Bearer " + jwtToken);
org.springframework.http.HttpEntity entity = new org.springframework.http.HttpEntity<>(headers);
return restTemplate.exchange(servicebUrl, org.springframework.http.HttpMethod.GET, entity, String.class).getBody();
}
private String generateServiceToken() {
Date expiresAt = new Date(System.currentTimeMillis() + 60000); // 1分钟有效期 (短)
Algorithm algorithm = Algorithm.RSA256(null, privateKey); // 使用RS256私钥
return JWT.create()
.withIssuer(issuer) // AuthService标识
.withSubject("service-a-id") // ServiceA的标识
.withAudience(servicebAudience) // 目标ServiceB
.withExpiresAt(expiresAt)
.withClaim("scope", "read:resource") // 自定义权限声明
.sign(algorithm);
}
}
7.3 服务 B:验证 JWT 并提取声明授权
ServiceBController:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.interfaces.RSAPublicKey; // 假设已获取
@RestController
public class ServiceBController {
@Value("${auth.service.issuer}")
private String expectedIssuer;
@Value("${serviceb.audience}")
private String expectedAudience;
private final RSAPublicKey publicKey;
public ServiceBController(RSAPublicKey publicKey) {
this.publicKey = publicKey;
}
@GetMapping("/serviceb/resource")
public String getProtectedResource() {
// 实际验证逻辑应在 Filter 或 Interceptor 中完成,这里简化演示
// 假设从请求头获取 Token
String authHeader = ...; // 从 HttpServletRequest 获取
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("Missing or invalid Authorization header");
}
String jwtToken = authHeader.substring(7);
try {
// 验证 Token
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(expectedIssuer)
.withAudience(expectedAudience)
.build();
DecodedJWT decodedJWT = verifier.verify(jwtToken);
// 检查权限声明 (例如 scope)
String scope = decodedJWT.getClaim("scope").asString();
if (scope == null || !scope.contains("read:resource")) {
throw new RuntimeException("Insufficient scope");
}
// 授权通过,返回受保护资源
return "This is the protected resource from ServiceB. Caller: " + decodedJWT.getSubject();
} catch (JWTVerificationException e) {
throw new RuntimeException("Invalid JWT: " + e.getMessage(), e);
}
}
}
7.4 代码实现与交互流程
- 密钥管理:
AuthService持有 RSA 私钥。ServiceA持有 RSA 私钥(仅用于模拟,实际应由AuthService签发)。ServiceB持有AuthService的 RSA 公钥(通过安全配置或 JWK 端点获取)。
- 交互流程:
ServiceA决定调用ServiceB。ServiceA生成(或向AuthService请求)一个包含iss,sub(A),aud(B),exp,scope的 JWT,并用私钥签名。ServiceA向ServiceB的/serviceb/resource发起 HTTP 请求,在Authorization: Bearer头中携带 JWT。ServiceB接收到请求。ServiceB从请求头中提取 JWT。ServiceB使用公钥验证 JWT 签名。ServiceB验证iss是否为可信的AuthService。ServiceB验证aud是否包含自己(或特定的 audience 值)。ServiceB验证exp确保 Token 未过期。ServiceB提取scope声明,检查是否包含read:resource权限。- 所有验证通过,
ServiceB处理请求并返回资源数据。 - 任何一步验证失败,
ServiceB返回错误(如 401/403)。
注意:实际微服务架构中,ServiceA 不应自己持有私钥签发 Token。通常由独立的认证授权服务器 (AuthService) 负责签发 Token。ServiceA 通过 OAuth2 Client Credentials 等流程从 AuthService 获取 Token。
8. 总结与资源
本指南详细介绍了 Java JWT 的原理、结构、核心概念、主流库 (java-jwt)、实战代码以及两个完整的应用案例(Web API 保护和微服务间认证)。
关键要点回顾:
- JWT 是一种无状态的、自包含的认证和授权令牌机制。
- 理解 Header、Payload (Claims)、Signature 三部分结构及其作用。
- 掌握使用
java-jwt库创建、解析和验证 JWT 的方法。 - 根据场景选择合适的签名算法 (
HS256,RS256)。 - 遵循密钥安全管理最佳实践。
- 实现令牌刷新机制以平衡安全性和用户体验。
- 在 Web 应用中,使用过滤器 (Filter) 验证 JWT 并保护 API。
- 在微服务间,使用 JWT 携带调用方身份和权限信息。
进一步学习资源:
- RFC 7519 - JSON Web Token (JWT)
- Auth0 java-jwt GitHub Repository & Documentation
- jwt.io:在线解码、验证和生成 JWT,查看不同库的支持。
- Spring Security OAuth2 / OAuth2 Resource Server:Spring 官方对 OAuth2 和 JWT 的支持。
希望这份详尽的指南能帮助你成功地在 Java 应用中集成 JWT!











