乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

OAuth2基础

定义与作用

OAuth2(Open Authorization 2.0) 是一种行业标准的授权协议,允许第三方应用在不暴露用户密码的前提下,代表用户获取对特定资源的有限访问权限。它通过引入"授权服务器"作为中间层,将用户身份认证与资源访问授权解耦,解决了传统自建用户体系中的多个核心痛点。

对比自建用户体系的痛点

痛点自建用户体系OAuth2方案
密码泄露风险用户需将密码直接交给第三方应用,存在泄密隐患用户仅在授权服务器输入密码,第三方应用永远接触不到密码
权限粒度控制要么全部开放,要么拒绝访问,无法精确控制数据范围通过 scope 机制限定权限范围(如仅读取头像、邮箱)
凭证撤销困难用户无法在不改密码的情况下单独收回某个第三方应用的权限在授权服务器撤销特定 Token 即可,不影响其他应用
多平台账号管理每个应用都需要独立的注册、登录、找回密码流程复用已有授权服务器账号(如 Google、GitHub),降低用户流失率

Spring Security 从 5.x 起内置了完整的 OAuth2 支持,包括 OAuth2 Client(客户端)、OAuth2 Resource Server(资源服务器)和 OAuth2 Authorization Server(授权服务器,5.x 推荐外部方案如 Keycloak)。


核心原理

四大核心角色

角色职责对应 Spring Security 组件
Resource Owner拥有受保护资源的用户,决定是否授权客户端访问最终用户,对应 Authentication.getPrincipal()
Client代表用户发起资源访问请求的应用程序OAuth2Client / ClientRegistration
Authorization Server负责用户认证、授权决策、颁发 Token外部身份提供商(如 Google、GitHub)或 Keycloak
Resource Server托管受保护资源,负责校验 Token 并返回资源OAuth2ResourceServer + JwtDecoder

四种授权模式

Spring Security 5.x 主要支持以下四种 OAuth2 授权模式,其中 Authorization Code(授权码模式) 和 Client Credentials(客户端凭证模式) 是生产环境最常用的两种:

授权模式适用场景安全性Spring Security 5.x 支持
Authorization Code服务端 Web 应用、第三方登录高(Token 不暴露给浏览器)✅ 核心支持
Client Credentials服务间 API 调用、后台任务高(无用户参与)✅ 核心支持
Implicit(隐式)纯前端 SPA(已弃用)低(Token 暴露在 URL)⚠️ 不推荐
Password(密码)高度信任的第一方应用(已弃用)中(需暴露密码)⚠️ 不推荐

Authorization Code 流程(授权码模式)

关键安全设计:

  1. code 只能使用一次,且有有效期(通常 10 分钟)
  2. client_secret 仅在服务端与授权服务器之间传输,浏览器不可见
  3. state 参数防止 CSRF 攻击,客户端必须验证回调的 state 与发送时一致

Client Credentials 流程(客户端凭证模式)

核心特点:无用户参与,客户端直接用自己的凭证换取 Token,适用于微服务间调用、定时任务等机器对机器(M2M)场景。


完整示例

示例一:Authorization Code 模式实现第三方登录

场景说明:企业内部管理系统需要支持员工使用公司统一身份认证平台(OIDC/OAuth2)登录,避免维护两套账号密码。

操作前配置(application.yml):

spring:
  security:
    oauth2:
      client:
        registration:
          corporate-idp:
            client-id: internal-app-client-id
            client-secret: internal-app-client-secret
            client-authentication-method: basic
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid, profile, email
            client-name: Corporate SSO
        provider:
          corporate-idp:
            authorization-uri: https://sso.company.com/oauth/authorize
            token-uri: https://sso.company.com/oauth/token
            user-info-uri: https://sso.company.com/oauth/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://sso.company.com/.well-known/jwks.json

操作后配置(Spring Security 5.x 配置类):

@Configuration
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authz -> authz
                .antMatchers("/", "/public/**", "/login/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );
    }
}

结果分析:

  1. 用户访问 /dashboard 时未登录,被重定向到 /login 页面
  2. 用户点击"Corporate SSO 登录",浏览器被重定向到 https://sso.company.com/oauth/authorize?response_type=code&client_id=internal-app-client-id&redirect_uri=...&scope=openid%20profile%20email&state=...
  3. 用户在统一认证平台完成登录并授权,携带 code 和 state 回调到 /login/oauth2/code/corporate-idp
  4. Spring Security 自动用 code + client_secret 向 token-uri 换取 access_token 和 id_token
  5. 根据 user-info-uri 获取用户信息,构造 OAuth2User 并存入 SecurityContextHolder
  6. 用户以 ROLE_USER 身份访问 /dashboard,认证成功

示例二:Client Credentials 模式实现服务间调用

场景说明:订单服务(Order Service)需要调用库存服务(Inventory Service)的 API 查询库存,两个服务之间通过 OAuth2 Client Credentials 模式进行安全认证。

操作前配置(库存服务作为 Resource Server,application.yml):

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.company.com

操作前配置(订单服务作为 Client,application.yml):

spring:
  security:
    oauth2:
      client:
        registration:
          inventory-api:
            client-id: order-service-client-id
            client-secret: order-service-client-secret
            client-authentication-method: basic
            authorization-grant-type: client_credentials
            scope: inventory.read
            token-uri: https://auth.company.com/oauth/token

操作后配置(订单服务发起调用):

@Service
public class InventoryClientService {

    private final WebClient webClient;

    public InventoryClientService(
            OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2.setDefaultClientRegistrationId("inventory-api");
        this.webClient = WebClient.builder()
            .filter(oauth2)
            .baseUrl("https://inventory.company.com")
            .build();
    }

    public Mono<InventoryResponse> checkStock(String productId) {
        return webClient.get()
            .uri("/api/inventory/{productId}", productId)
            .retrieve()
            .bodyToMono(InventoryResponse.class);
    }
}

@Configuration
public class OAuth2ClientConfig {

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {

        OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
}

结果分析:

  1. 订单服务调用库存 API 时,WebClient 过滤器自动检查 OAuth2AuthorizedClientRepository 中是否存在有效 Token
  2. 若 Token 不存在或已过期,过滤器自动用 client_credentials 向 https://auth.company.com/oauth/token 请求新 Token
  3. 请求体携带 grant_type=client_credentials&client_id=order-service-client-id&client_secret=order-service-client-secret&scope=inventory.read
  4. 获取 access_token 后,自动在请求头中附加 Authorization: Bearer <token>
  5. 库存服务(Resource Server)收到请求后,使用 JwtDecoder 校验 Token 签名、issuer、过期时间,并检查 scope 是否包含 inventory.read
  6. 校验通过返回库存数据,全程无用户参与,实现安全的机器对机器通信

易错场景与面试考点

易错场景:Authorization Code 回调阶段 state 参数不匹配导致登录失败

在 Authorization Code 流程中,Spring Security 5.x 的 OAuth2AuthorizationRequestRedirectFilter 会在重定向用户到授权服务器前生成一个随机的 state 参数并存入 HttpSessionOAuth2AuthorizationRequestRepository。当用户完成授权后回调时,如果回调请求中的 state 与 Session 中存储的不一致,Spring Security 会抛出 OAuth2AuthenticationException 并拒绝登录。

常见触发原因:

  • 多实例部署时未配置 Session 共享(Sticky Session 失效导致请求落到另一台实例)
  • 客户端在授权过程中意外清除了 Cookie 或 Session
  • 自定义 OAuth2AuthorizationRequestRepository 时未正确实现 state 的保存与比对

调试方法:

// 在配置类中开启 OAuth2 登录调试日志
logging:
  level:
    org.springframework.security.oauth2: DEBUG

查看日志中是否有 Authorization request state not matching 或 Missing state parameter 关键字。解决多实例问题需配置 Spring Session Redis 或基于数据库的 OAuth2AuthorizationRequestRepository 实现。

面试考点:

  • 面试官常问:OAuth2 的 state 参数和 PKCE(Proof Key for Code Exchange)有什么区别?
  • 答:state 用于防止 CSRF 攻击,确保回调请求确实是当前用户发起的;PKCE 是授权码模式的扩展,在公共客户端(如移动端、SPA)中防止授权码被拦截后盗用,通过 code_challenge 和 code_verifier 机制确保只有原始请求者能换取 Token。

版本说明:本节基于 Spring Security 5.x 编写。Spring Security 5.x 中 WebSecurityConfigurerAdapter 和 antMatchers 是标准配置方式。若迁移到 6.x,需将 WebSecurityConfigurerAdapter 改为 SecurityFilterChain Bean,并将 antMatchers 替换为 requestMatchers。

上一页
本章定位
下一页
OAuth2 Client