Spring Boot: OAuth2 与 JWT 认证

最后更新:2026-08-26

JWT 是微服务的通行证——无状态、自包含、跨服务验证,告别 Session 共享难题。

1. 你将学到


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 授权流程

100%
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 编码,任何人都能解码,只靠签名保证完整性。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 实现 JWT 登录和接口认证,替换 httpBasic,使用 curl 测试登录获取 Token → 携带 Token 访问受保护接口。

  2. 进阶题(难度⭐⭐):实现 Refresh Token 机制——Access Token 有效期 15 分钟,Refresh Token 有效期 7 天,Refresh Token 用于获取新的 Access Token。

  3. 挑战题(难度⭐⭐⭐):集成 Keycloak 作为外部 Authorization Server,OrderFlow 仅作为 Resource Server 验证 Keycloak 签发的 JWT,思考服务间信任关系的建立方式。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏