人人都会AI编程

15.5 OAuth2.0 与单点登录基础

更新时间:2026-07-10

微服务和前后端分离架构的普及,使得“认证”与“授权”不再是单个应用内部的事情。用户在一个系统登录后,期望无缝访问其他关联系统;第三方应用也希望在用户授权下获取其数据,而无需拿到用户的密码。OAuth 2.0 正是解决这类问题的工业标准协议,而 Spring Security 对其提供了完整的支持。本节将从一个可运行的实战角度,梳理 OAuth 2.0 的核心概念、四种授权模式,以及如何基于 Spring Security 快速搭建单点登录(SSO)的基础骨架。

15.5.1 OAuth 2.0 的核心角色与流程

理解 OAuth 2.0,首先要理清它定义的四个角色:

  • 资源拥有者(Resource Owner):通常是终端用户,拥有受保护资源(如头像、订单数据)的所有权。
  • 客户端(Client):想要访问用户资源的第三方应用,可以是移动 App、Web 前端、后端服务等。
  • 授权服务器(Authorization Server):负责对用户进行身份认证,并向客户端颁发访问令牌(Access Token)。
  • 资源服务器(Resource Server):托管受保护资源,接收并验证访问令牌,决定是否返回资源。

一次典型的授权过程,简化描述就是:客户端将用户导向授权服务器,用户登录并同意授权后,授权服务器向客户端颁发一个令牌。客户端携带这个令牌去访问资源服务器,资源服务器验证令牌有效后,提供对应的资源。整个过程中,客户端从未接触过用户的密码——这就是 OAuth 2.0 最大的安全价值。

15.5.2 四种授权模式与适用场景

OAuth 2.0 定义了四种授权模式,以适应不同的客户端类型和安全等级。实际开发中绝大多数场景只会用到前两种。

1. 授权码模式(Authorization Code)

这是功能最完整、安全性最高的模式,也是目前 Spring Security 默认支持的模式。它适用于有后端服务器的客户端(如传统的 Web 应用)。

流程分为两步:首先,客户端将用户浏览器重定向到授权服务器,用户登录并授权后,授权服务器通过浏览器重定向将授权码发给客户端;然后,客户端在后端用这个授权码加上自己的客户端凭据(client_id 和 client_secret),向授权服务器的令牌端点直接换取访问令牌刷新令牌。因为令牌的传递只发生在后端,不会暴露在浏览器 URL 或浏览器历史中,安全性高。

2. 客户端凭据模式(Client Credentials)

当客户端本身就是一个受信任的后端服务,而非代表某个用户时,使用此模式。客户端直接用自身的 client_id 和 client_secret 向授权服务器申请访问令牌,全程不涉及用户交互。它很适合服务间调用(如订单服务调用库存服务)。

3. 密码模式(Resource Owner Password Credentials)

用户将自己的用户名和密码直接告诉客户端,客户端凭此换取令牌。这种模式要求客户端高度受信任(如官方出品的原生 App),在现代架构中已不推荐使用,因为将用户凭据暴露给了客户端。

4. 隐式模式(Implicit)

这是授权码模式的简化版,用于没有后端服务的纯前端应用(如 SPA),令牌直接通过浏览器重定向返回。由于令牌会暴露在浏览器中,安全性较低,OAuth 2.1 草案已将其废弃,推荐使用授权码模式 + PKCE 替代。

现实中的最佳实践:凡是能使用授权码模式的地方,都应优先使用授权码模式。对于移动 App 或 SPA,配合 PKCE(Proof Key for Code Exchange)同样可以使用授权码模式,兼顾安全与便捷。

15.5.3 Spring Security 中的 OAuth 2.0 落地

Spring Security 从 5.x 版本开始,将 OAuth 2.0 的支持整合到了 spring-security-oauth2-clientspring-security-oauth2-resource-server 模块中,并推出了新的 spring-security-oauth2-authorization-server 项目。这意味着一站式搭建授权服务器、资源服务器和客户端成为可能。

1. 搭建授权服务器

引入依赖后,通过简单的配置类即可启动一个带默认行为的授权服务器:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("my-web-app")
                .clientSecret("{noop}secret")  // 生产环境必须使用编码后的密码
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/my-web-app")
                .scope("read")
                .build();
        return new InMemoryRegisteredClientRepository(client);
    }
}

这段配置做了三件事:启用授权服务器;注册了一个客户端应用(my-web-app),使其支持授权码和刷新令牌模式;配置了基本的表单登录以验证用户身份。实际生产环境需要将客户端信息存入数据库,并替换密码编码策略。

2. 保护资源服务器

资源服务器通过验证 JWT 令牌来保护接口:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9000

然后在 Security 配置中声明资源服务器过滤器:

@Bean
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

此时,任何客户端携带从授权服务器获取的合法 JWT,就可以访问受保护的资源。

15.5.4 单点登录(SSO)的实现原理

单点登录(Single Sign-On)的本质是多个应用共享同一个认证中心。用户只需在认证中心登录一次,就能自由访问所有信任该中心的应用。OAuth 2.0 的授权码模式天然就支持这种场景:每一个客户端应用都将用户重定向到同一个授权服务器完成登录,登录成功后,用户在这个授权服务器域下会有会话(Cookie),后续到其他客户端时,授权服务器会识别出已登录状态,直接跳过登录步骤,返回授权码。整个过程对用户来说就是“只输了一次密码”。

基于 Spring Security 实现 SSO 的步骤非常简单:

  1. 搭建统一的授权服务器(如前所示)。
  2. 在各个客户端应用中配置相同的 OAuth 2.0 客户端属性
spring:
  security:
    oauth2:
      client:
        registration:
          sso-provider:
            client-id: my-web-app
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: read
        provider:
          sso-provider:
            authorization-uri: http://auth-server:9000/oauth2/authorize
            token-uri: http://auth-server:9000/oauth2/token
            user-info-uri: http://auth-server:9000/userinfo
            user-name-attribute: sub

客户端只需引入 spring-boot-starter-oauth2-client,Spring Security 会自动装配登录过滤器。用户访问客户端的任何受保护页面,都会被重定向到 http://auth-server:9000/oauth2/authorize,登录后回调本地的 /login/oauth2/code/sso-provider,完成本地会话的建立。多个客户端共享同一个认证中心,SSO 即告实现。

15.5.5 实战细节与注意事项

  1. 令牌存储策略:授权服务器生成的令牌如果采用默认的内存存储,重启后全部失效。生产环境应配置数据库或 Redis 持久化令牌,并对 JWT 签名使用非对称密钥(RSA 密钥对),以便资源服务器无需调用授权服务器即可独立验证令牌。
  1. 安全退出:单点登录的难点往往在于“退出”。简单的做法是清除本地会话,真正的全局退出需要授权服务器支持 RP-Initiated Logout(利用 OIDC 的 end_session_endpoint),这已超出基础范畴,但在选型时应当留意。
  1. OIDC 的补充:OpenID Connect(OIDC)是建立在 OAuth 2.0 之上的身份认证层,它标准化了用户信息端点、ID Token 等,Spring Security 同时支持 OAuth 2.0 和 OIDC。如果只是做登录认证,直接使用 OIDC 可以少踩不少坑。
  1. CSRF 与重定向安全:使用授权码模式时,Spring Security 会自动验证 state 参数以防止 CSRF 攻击。确保不要关闭此特性。
  1. 网关统一鉴权:在微服务网关(如 Spring Cloud Gateway)层集中验证 JWT,后端服务可以声明为资源服务器,但只需配置资源服务器所需的 JWT 解码公钥,不必再重复引入客户端依赖。

OAuth 2.0 协议本身概念较多,但 Spring Security 已将大部分复杂度封装为自动配置与默认行为。初次接触时,建议先按照授权码模式 + 单机授权服务器跑通整个流程,再逐步替换持久化存储、引入网关和定制授权页面。这样既能快速建立感性认知,也能为后续生产化打下扎实基础。