1. 简介

OAuth 是一个开放标准,描述了授权流程。它常用于 API 的用户访问授权,例如 REST API 可以限制仅允许拥有适当角色的注册用户访问。

OAuth 授权服务器负责用户认证,并颁发包含用户数据和访问策略的访问令牌(Access Token)。

本文将基于 Spring Security OAuth Authorization Server 实现一个简单的 OAuth 应用。

在这个过程中,我们将构建一个客户端-服务端应用,从 REST API 中获取 Baeldung 文章列表。客户端和服务端都需要进行 OAuth 认证。

2. 授权服务器实现

我们首先来看 OAuth 授权服务器的配置。它将作为文章资源服务器和客户端服务器的统一认证源。

2.1. Maven 依赖

首先,在 pom.xml 文件中添加以下依赖:

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

2.2. 配置文件

首先创建 application.yml 文件,设置授权服务器监听端口为 9000:

server:
  port: 9000

接着,由于每个授权服务器需要一个唯一的 issuer URL,我们通过以下配置指定:

spring:
  security:
    oauth2:
      authorizationserver:
        issuer: http://auth-server:9000

⚠️ 同时需要在本地 /etc/hosts 文件中添加一行:

127.0.0.1 auth-server

这样可以避免本地运行时客户端和授权服务器之间的 session cookie 冲突。

接下来配置客户端服务的注册信息。本例中我们只注册一个名为 articles-client 的客户端:

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          articles-client:
            registration:
              client-id: articles-client
              client-secret: "{noop}secret"
              client-name: Articles Client
              client-authentication-methods:
                - client_secret_basic
              authorization-grant-types:
                - authorization_code
                - refresh_token
              redirect-uris:
                - http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc
                - http://127.0.0.1:8080/authorized
              scopes:
                - openid
                - articles.read

配置项说明如下:

Client ID:用于标识客户端身份
Client Secret:客户端与服务端共享的密钥,用于建立信任关系
Authentication Method:本例使用 basic 认证(用户名 + 密码)
Grant Types:支持授权码和刷新令牌两种授权方式
Redirect URI:客户端用于重定向的地址
Scope:定义客户端可访问的权限范围,包括标准的 openid 和自定义的 articles.read

然后我们进入 Spring Bean 配置部分。

首先启用 Spring Security 模块,使用 @Configuration@EnableWebSecurity 注解:

@Configuration
@EnableWebSecurity
public class DefaultSecurityConfig {
    // ...
}

接着配置 Spring Security 过滤器链,启用默认的 OAuth 安全配置,并生成默认登录页面:

@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
      .oidc(withDefaults()); // 启用 OpenID Connect 1.0
    return http.formLogin(withDefaults()).build();
}

再配置第二个安全过滤器链用于用户认证:

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

这里我们调用 authorizeRequests.anyRequest().authenticated() 要求所有请求都需要认证,并通过 formLogin(withDefaults()) 提供表单登录。

最后定义一个测试用的用户。本例中我们创建一个内存用户:

@Bean
UserDetailsService users() {
    PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    UserDetails user = User.builder()
      .username("admin")
      .password("password")
      .passwordEncoder(encoder::encode)
      .roles("USER")
      .build();
    return new InMemoryUserDetailsManager(user);
}

3. 资源服务器

接下来我们创建一个资源服务器,提供一个返回文章列表的 GET 接口。该接口只允许通过 OAuth 认证的请求访问。

3.1. Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>3.2.2</version>
</dependency>

3.2. 配置文件

application.yml 中配置端口和认证服务器地址:

server:
  port: 8090

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

然后配置 Web 安全策略,确保 /articles/** 接口只允许拥有 articles.read 权限的请求访问:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/articles/**")
          .authorizeHttpRequests(authorize -> authorize.anyRequest()
            .hasAuthority("SCOPE_articles.read"))
          .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

这里调用了 oauth2ResourceServer() 方法,会根据 application.yml 自动配置与授权服务器的连接。

3.3. 文章接口控制器

创建一个 REST 控制器,提供 /articles 接口:

@RestController
public class ArticlesController {

    @GetMapping("/articles")
    public String[] getArticles() {
        return new String[] { "Article 1", "Article 2", "Article 3" };
    }
}

4. 客户端应用

最后我们创建一个客户端应用,用于从资源服务器获取文章列表。

4.1. Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>3.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webflux</artifactId>
    <version>6.1.3</version>
</dependency>
<dependency>
    <groupId>io.projectreactor.netty</groupId>
    <artifactId>reactor-netty</artifactId>
    <version>1.1.15</version>
</dependency>

4.2. 配置文件

配置客户端认证信息:

server:
  port: 8080

spring:
  security:
    oauth2:
      client:
        registration:
          articles-client-oidc:
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
            scope: openid
            client-name: articles-client-oidc
          articles-client-authorization-code:
            provider: spring
            client-id: articles-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/authorized"
            scope: articles.read
            client-name: articles-client-authorization-code
        provider:
          spring:
            issuer-uri: http://auth-server:9000

创建 WebClient 实例,用于向资源服务器发送请求,并添加 OAuth 认证过滤器:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
      new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    return WebClient.builder()
      .apply(oauth2Client.oauth2Configuration())
      .build();
}

WebClient 依赖 OAuth2AuthorizedClientManager,我们创建默认实现:

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

    OAuth2AuthorizedClientProvider authorizedClientProvider =
      OAuth2AuthorizedClientProviderBuilder.builder()
        .authorizationCode()
        .refreshToken()
        .build();
    DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
      clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

最后配置 Web 安全:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
          .authorizeHttpRequests(authorizeRequests ->
            authorizeRequests.anyRequest().authenticated()
          )
          .oauth2Login(oauth2Login ->
            oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))
          .oauth2Client(withDefaults());
        return http.build();
    }
}

4.3. 文章客户端控制器

创建一个控制器,使用 WebClient 向资源服务器发送请求并获取文章列表:

@RestController
public class ArticlesController {

    private WebClient webClient;

    @GetMapping(value = "/articles")
    public String[] getArticles(
      @RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
    ) {
        return this.webClient
          .get()
          .uri("http://127.0.0.1:8090/articles")
          .attributes(oauth2AuthorizedClient(authorizedClient))
          .retrieve()
          .bodyToMono(String[].class)
          .block();
    }
}

这里通过 @RegisteredOAuth2AuthorizedClient 注解自动绑定授权客户端,并将其用于 HTTP 请求的认证。

4.4. 访问文章列表

访问 http://127.0.0.1:8080/articles 时,会自动跳转到授权服务器登录页:

loginPage

登录成功后,将跳转回目标页面并显示文章列表。后续请求将自动使用 cookie 中的 token,无需重复登录。

5. 总结

本文介绍了如何使用 Spring Security OAuth Authorization Server 实现完整的 OAuth 授权流程。

✅ 完整源码可从 GitHub 获取。


原始标题:Spring Security OAuth Authorization Server