Spring Boot: OAuth2 与 JWT 认证
最后更新:2026-08-26
JWT 是微服务的通行证——无状态、自包含、跨服务验证,告别 Session 共享难题。
1. 你将学到
- OAuth2 授权码流程与密码模式对比
- JWT 令牌结构:Header / Payload / Signature
spring-boot-starter-oauth2-resource-server配置与 JWT 解码- 自定义 JWT Claims 与用户角色映射
- Alice 实现 OrderFlow 的登录签发 JWT 与接口令牌验证
2. 一个架构师的真实故事
(1) 痛点:Session 在微服务中失效
Alice 将 OrderFlow 从单实例扩展到 3 个实例后,Session 问题来了:用户在实例 A 登录,请求被负载均衡到实例 B 时 Session 不存在,用户被踢出。Bob 尝试用 Redis 共享 Session,但引入了新的依赖和复杂性。Charlie 要求支持第三方登录(Google/GitHub),传统 Session 方案完全无法满足。
(2) JWT 的解法
JWT 令牌自包含所有认证信息,服务端无需存储 Session:
JAVA
// Issue JWT on login
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
// Verify JWT on each request (stateless, no session needed)
// Spring Security auto-extracts roles from JWT claims
(3) 收益
Alice 用 JWT 替换 Session 后:3 个实例无需共享 Session,水平扩展零成本;客户端存储令牌,服务端无状态;支持第三方 OAuth2 登录。
3. OAuth2 与 JWT 概念
(1) OAuth2 授权流程
sequenceDiagram
participant U as User Agent<br/>(Browser)
participant C as Client<br/>(OrderFlow SPA)
participant AS as Authorization<br/>Server
participant RS as Resource<br/>Server (API)
U->>C: Click "Login with GitHub"
C->>AS: Redirect to /authorize
AS->>U: Login + Consent page
U->>AS: Approve
AS->>C: Redirect back with auth code
C->>AS: POST /token (auth code + client_secret)
AS->>C: Access Token (JWT)
C->>RS: GET /api/orders (Bearer token)
RS->>RS: Verify JWT signature
RS->>C: 200 OK + Data
| 授权模式 | 适用场景 | 客户端类型 |
|---|---|---|
| 授权码模式(Authorization Code) | Web 应用 / SPA | 后端服务 + 前端 |
| 客户端凭证模式(Client Credentials) | 服务间调用 | 机器对机器 |
| 资源所有者密码模式(ROPC) | 已弃用,仅测试 | 第一方应用 |
| 隐式模式(Implicit) | 已弃用 | 旧 SPA |
(2) JWT 令牌结构
JWT 由三部分组成,用 . 分隔:
TEXT
📖 仅展示
Header.Payload.Signature
eyJhbGciOiJS256.eyJzdWIiOiJib2IiLCJyb2xlIjoiQ1VTVU9NRVIifQ.signature
| 部分 | 内容 | 示例 |
|---|---|---|
| Header | 算法 + 令牌类型 | {"alg":"RS256","typ":"JWT"} |
| Payload | Claims(声明) | {"sub":"bob","role":"CUSTOMER","exp":1705312000} |
| Signature | Header+Payload 的签名 | RS256 签名验证完整性 |
| 标准 Claim | 含义 |
|---|---|
sub |
Subject(用户标识) |
iss |
Issuer(签发者) |
aud |
Audience(受众) |
exp |
Expiration(过期时间) |
iat |
Issued At(签发时间) |
scope |
权限范围 |
4. OAuth2 Resource Server 配置
(1) 依赖与配置
▶ 示例: pom.xml 依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: SecurityFilterChain JWT 配置
JAVA
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
String role = jwt.getClaimAsString("role");
if (role == null) return List.of();
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
});
return converter;
}
}
输出:
TEXT
📖 仅展示
// 执行成功
5. JWT 签发与验证
(1) RSA 密钥对配置
▶ 示例: 生成 RSA 密钥对
JAVA
@Configuration
public class JwtConfig {
@Bean
public KeyPair keyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
}
@Bean
public JwtEncoder jwtEncoder(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSource<SecurityContext> jwkSource =
new ImmutableJWKSet<>(new JWKSet(rsaKey));
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return JwtDecoders.withPublicKey((RSAPublicKey) keyPair.getPublic());
}
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: 登录签发 JWT
JAVA
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authenticationManager;
public AuthController(JwtEncoder jwtEncoder,
AuthenticationManager authenticationManager) {
this.jwtEncoder = jwtEncoder;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
Instant now = Instant.now();
Instant expiry = now.plus(1, ChronoUnit.HOURS);
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow-service")
.subject(auth.getName())
.issuedAt(now)
.expiresAt(expiry)
.claim("role", auth.getAuthorities().stream()
.filter(a -> a.getAuthority().startsWith("ROLE_"))
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder
.encode(JwtEncoderParameters.from(claims))
.getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
输出:
TEXT
📖 仅展示
// 执行成功
6. 自定义 JWT Claims 与角色映射
(1) 角色映射策略
| Claim 字段 | 值 | 映射为 Authority |
|---|---|---|
role: "ADMIN" |
单角色字符串 | ROLE_ADMIN |
roles: ["ADMIN","CUSTOMER"] |
角色数组 | ROLE_ADMIN, ROLE_CUSTOMER |
scope: "read write" |
OAuth2 scope | SCOPE_read, SCOPE_write |
▶ 示例: 自定义 JwtAuthenticationConverter
JAVA
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
// Support both "role" (single) and "roles" (array) claims
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
String role = jwt.getClaimAsString("role");
if (role != null) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
return authorities;
});
return converter;
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: 在 Controller 中使用 JWT 信息
JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/my")
public List<OrderResponse> getMyOrders(
@AuthenticationPrincipal Jwt jwt) {
String username = jwt.getClaimAsString("sub");
String role = jwt.getClaimAsString("role");
return orderService.getOrdersByUsername(username);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| 方式 | 获取用户信息 | 适用场景 |
|---|---|---|
@AuthenticationPrincipal Jwt jwt |
完整 JWT Claims | 需要自定义 Claim |
@AuthenticationPrincipal UserDetails user |
UserDetails | 需要标准用户信息 |
Principal principal |
getName() | 只需用户名 |
7. 综合示例:OrderFlow JWT 认证完整实现
JAVA
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt ->
Optional.ofNullable(jwt.getClaimAsString("role"))
.map(role -> List.of(new SimpleGrantedAuthority("ROLE_" + role)))
.orElse(List.of()));
return converter;
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService, PasswordEncoder encoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}
}
// AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final JwtEncoder jwtEncoder;
private final AuthenticationManager authManager;
public AuthController(JwtEncoder jwtEncoder, AuthenticationManager authManager) {
this.jwtEncoder = jwtEncoder;
this.authManager = authManager;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody LoginRequest req) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password()));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("orderflow").subject(auth.getName())
.issuedAt(now).expiresAt(now.plus(1, ChronoUnit.HOURS))
.claim("role", auth.getAuthorities().stream()
.map(a -> a.getAuthority().replace("ROLE_", ""))
.findFirst().orElse("CUSTOMER"))
.build();
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
return Map.of("accessToken", token);
}
public record LoginRequest(String username, String password) {}
}
💻 测试流程:
BASH
# 1. Login and get JWT
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"pass123"}' | jq -r '.accessToken')
# 2. Access protected API with JWT
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/orders/my
# 3. Admin endpoint with admin JWT
ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"admin123"}' | jq -r '.accessToken')
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:8080/api/v1/admin/dashboard
❓ 常见问题
Q JWT 和 Session 的核心区别是什么?
A Session 有状态(服务端存储),JWT 无状态(令牌自包含)。JWT 适合微服务和水平扩展,Session 适合单体应用。JWT 无法主动撤销(除非引入黑名单),Session 可以即时销毁。
Q JWT 使用对称加密还是非对称加密?
A 生产环境推荐非对称加密(RSA/ECDSA)。Resource Server 只需公钥验证,私钥只在 Authorization Server 保管。对称加密(HMAC)密钥泄露风险大。
Q JWT 过期后怎么办?
A 常用方案:1)Refresh Token 机制,短期 Access Token + 长期 Refresh Token;2)客户端检测过期自动跳转登录页;3)设置合理的过期时间(1-4 小时)。
Q 如何撤销已签发的 JWT?
A JWT 本身无法撤销。常见方案:1)短过期时间 + Refresh Token;2)Token 黑名单(Redis 存储);3)修改签名密钥使所有旧 Token 失效。
Q OAuth2 Resource Server 和 Authorization Server 是什么关系?
A Authorization Server 负责签发令牌,Resource Server 负责验证令牌保护资源。本课 OrderFlow 既是 Resource Server 也简化实现了 Authorization Server。生产环境建议使用 Keycloak 等专业方案。
Q JWT 的 Claims 放多少信息合适?
A 最少原则。只放用户标识(sub)和角色(role),不放敏感信息(密码、手机号)。JWT 是 Base64 编码,任何人都能解码,只靠签名保证完整性。
📖 小节
- JWT 三部分:Header(算法)、Payload(Claims)、Signature(签名)
- OAuth2 Resource Server 验证 JWT 签名,从 Claims 提取角色进行授权
- RSA 非对称加密:私钥签发,公钥验证,密钥管理更安全
JwtAuthenticationConverter自定义 Claim 到 Authority 的映射逻辑- 登录接口签发 JWT,其他接口通过 Bearer Token 认证
- JWT 无状态,适合微服务;缺点是无法主动撤销
📝 作业
-
基础题(难度⭐):为 OrderFlow 实现 JWT 登录和接口认证,替换 httpBasic,使用 curl 测试登录获取 Token → 携带 Token 访问受保护接口。
-
进阶题(难度⭐⭐):实现 Refresh Token 机制——Access Token 有效期 15 分钟,Refresh Token 有效期 7 天,Refresh Token 用于获取新的 Access Token。
-
挑战题(难度⭐⭐⭐):集成 Keycloak 作为外部 Authorization Server,OrderFlow 仅作为 Resource Server 验证 Keycloak 签发的 JWT,思考服务间信任关系的建立方式。