从 OAuth 到 Spring Authorization Server:搭建授权服务器时真正要想清楚的事

2026-08-27 32 预计阅读时间: 1 分钟
来源: spring.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

围绕 Joe Grandja 的讨论,Spring Authorization Server、OAuth 以及现代身份认证体系被放在了同一张技术地图上。对开发者来说,重点不只是“如何发出一个 access token”,而是理解授权服务器在客户端、资源服务器和最终用户之间承担的边界,并把这些边界落实到可验证的配置与代码中。

OAuth 解决的是什么问题

OAuth 2.0 解决的是“一个应用如何代表用户访问受保护资源”的授权问题。它并不等同于登录协议:

  • 资源所有者通常是最终用户。
  • 客户端是请求访问资源的应用,例如 Web 应用、移动应用或后台服务。
  • 授权服务器负责验证用户和客户端,并签发授权结果。
  • 资源服务器负责验证 access token,并提供受保护的 API。

这个角色划分很重要。业务 API 不应该自己重新实现登录页面、授权码交换、token 签发和 token 撤销逻辑;这些职责集中到授权服务器后,资源服务器可以专注于验证令牌和执行业务授权。

OAuth 的具体流程取决于客户端类型。对于有用户参与的 Web 应用,授权码模式通常是更合适的基础;对于没有用户交互的服务间调用,则可以考虑客户端凭证模式。所谓“支持 OAuth”,不能只看是否返回了 JWT,还要看客户端认证、重定向地址、授权范围、令牌生命周期和撤销策略是否完整。

Spring Authorization Server 的定位

Spring Authorization Server 是 Spring 生态中用于构建 OAuth 2.0 和 OpenID Connect 授权服务器的项目。实际采用时,可以把它看成一组授权服务器基础能力,而不是一个开箱即用的业务身份平台。

它适合处理协议层面的工作,例如:

  • 注册和认证 OAuth 客户端。
  • 处理授权码和 token 请求。
  • 根据授权范围签发令牌。
  • 配置令牌端点、授权端点和客户端认证方式。
  • 在需要时提供 OpenID Connect 相关能力。

但用户目录、密码策略、多因素认证、组织与租户模型、管理员界面、审计报表以及生产环境密钥托管,仍然需要结合应用自身或外部身份系统设计。Spring Security 负责的用户认证机制,也需要和授权服务器的协议配置配合起来,而不是把两者混成一个过滤器链问题。

一个可改造的最小配置

下面示例展示一个适合本地验证概念的 Spring Boot 配置。它假设项目已经引入 Spring Boot、Spring Security 和 Spring Authorization Server 依赖,用户认证使用内存用户,客户端信息也保存在内存中。生产环境应替换为数据库、外部身份目录以及持久化密钥存储。

Maven 依赖

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
  </dependency>
</dependencies>

Java 配置

package com.example.auth;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class AuthorizationServerConfig {

  @Bean
  @Order(1)
  SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
      throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(
            org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers
                .OAuth2AuthorizationServerConfigurer.class)
        .oidc(Customizer.withDefaults());
    return http.formLogin(Customizer.withDefaults()).build();
  }

  @Bean
  @Order(2)
  SecurityFilterChain applicationSecurityFilterChain(HttpSecurity http)
      throws Exception {
    http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults());
    return http.build();
  }

  @Bean
  RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
    RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("demo-client")
        .clientSecret(encoder.encode("demo-secret"))
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .scope("orders.read")
        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
        .build();
    return new InMemoryRegisteredClientRepository(client);
  }

  @Bean
  UserDetailsService users(PasswordEncoder encoder) {
    return new InMemoryUserDetailsManager(
        User.withUsername("alice")
            .password(encoder.encode("change-me"))
            .roles("USER")
            .build());
  }

  @Bean
  PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

  @Bean
  JWKSource<SecurityContext> jwkSource() throws Exception {
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(2048);
    KeyPair keyPair = generator.generateKeyPair();
    RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
        .privateKey((RSAPrivateKey) keyPair.getPrivate())
        .keyID(UUID.randomUUID().toString())
        .build();
    JWKSet set = new JWKSet(rsaKey);
    return (selector, context) -> selector.select(set);
  }
}

启动服务后,可以用客户端凭证模式获取一个访问令牌。下面的命令适合本地测试:

curl -u demo-client:demo-secret \
  -d "grant_type=client_credentials&scope=orders.read" \
  http://localhost:8080/oauth2/token

示例中的 RSA 密钥在每次进程启动时重新生成,因此重启服务后之前签发的令牌可能无法继续验证。这种配置只用于演示协议流程。生产环境必须使用稳定的密钥生命周期管理,并考虑密钥轮换、旧密钥保留时间和 JWKS 发布策略。

设计时不能跳过的边界

登录和授权要分开

OAuth 处理的是授权委托。如果应用需要确认“这个人是谁”,应使用 OpenID Connect 或应用自身的会话机制,而不是把 access token 当作登录凭证。资源服务器也不应仅凭 token 中的用户名就决定所有业务权限,还应结合角色、租户、资源归属和当前操作进行判断。

客户端类型决定安全策略

浏览器和移动端通常无法安全保存长期客户端密钥,应该采用适合公开客户端的方案,并使用 PKCE 保护授权码交换。后端服务可以使用客户端认证,但密钥仍应放在密钥管理系统或安全的运行环境中,不能提交到代码仓库。

令牌内容不是权限模型

JWT 便于资源服务器本地验证,但它也会让权限信息在有效期内保持不变。权限变化、用户禁用和紧急撤销都需要额外策略。可以缩短 access token 生命周期、使用 refresh token 轮换、维护撤销机制,或者选择由资源服务器回查授权服务器的令牌状态。

落地清单

引入 Spring Authorization Server 前,可以逐项确认:

  • 已明确哪些系统是授权服务器,哪些系统是资源服务器。
  • 每个客户端都有独立的身份、重定向地址和授权范围。
  • 授权范围与业务权限之间有明确映射,而不是随意授予全部权限。
  • 生产密钥不会随应用启动随机生成,也不会硬编码在仓库里。
  • 已决定 access token、refresh token 和授权码的生命周期。
  • 已设计用户禁用、令牌撤销、密钥轮换和审计方案。
  • 已为授权码重放、开放重定向、过宽 scope 和日志泄露建立测试。

Spring Authorization Server 的价值在于把 OAuth 和 OpenID Connect 的协议实现纳入 Spring Security 的工程体系。真正困难的部分仍然是身份边界、权限模型和运维策略。先把角色与流程画清楚,再逐步替换内存组件、补齐密钥与审计能力,通常比一开始堆叠复杂配置更容易得到可验证、可维护的授权服务。


相关推荐