Spring Security Oauth 授权服务器

1、简介

OAuth 是一种描述授权过程的开放标准。它可用于授权用户访问 API。例如,REST API 可以限制只有具有适当角色的注册用户才能访问。

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

本将带你了解如何使用 Spring Security OAuth 授权服务器 实现一个简单的 OAuth 应用。

我们要创建一个 CS 应用,通过 REST API 获取资源服务器上的文章列表。客户端服务和服务器服务都需要 OAuth 身份认证。

2、授权服务器实现

先来看看 OAuth 授权服务器的配置。它作为文章资源和客户端服务器的身份认证源。

2.1、依赖

首先,在 pom.xml 中添加如下依赖:

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

2.2、配置

application.yml 文件中,设置 server.port 属性来配置认证服务器的运行端口:

server:
  port: 9000

然后,就可以开始配置 Spring Bean 了。首先,需要一个 @Configuration 类,在该类中创建一些 OAuth 特有的 Bean。

第一个是客户端服务的 Repository。在本例中,使用 RegisteredClient Builder 类创建一个客户端:

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
          .clientId("articles-client")
          .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/articles-client-oidc")
          .redirectUri("http://127.0.0.1:8080/authorized")
          .scope(OidcScopes.OPENID)
          .scope("articles.read")
          .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }
}

配置的属性如下:

  • 客户端 ID(Client ID )- Spring 将用它来识别哪个客户端正在尝试访问资源
  • 客户端 Secret Code(Client secret code) - 客户端和服务器都知道的 Secret Code,提供双方之间的信任
  • 认证方法(Authentication method) - 使用 Basic Authentication,即用户名和密码
  • 授权模式(Authorization Grant Type) - 允许客户端同时生成授权码(Authorization Code)和刷新令牌(Refresh Token)
  • 重定向 URI(Redirect URI) - 客户端在基于重定向的模式中使用它
  • 范围(Scope)- 该参数定义了客户端可能拥有的授权。在本例中,拥有 OidcScopes.OPENID 和自定义的 articles.read

接下来,配置一个 Bean 来应用默认的 OAuth Security,并生成一个默认的表单登录页面:

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

每个授权服务器都需要自己的 Token 签名密钥,以保持安全域之间的适当边界。

生成一个 2048 字节的 RSA 密钥:

@Bean
public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
      .privateKey(privateKey)
      .keyID(UUID.randomUUID().toString())
      .build();
}

private static KeyPair generateRsaKey() {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    return keyPairGenerator.generateKeyPair();
}

除了签名密钥,每个授权服务器还需要有一个唯一的 issuer URL。

创建 ProviderSettings Bean 将其设置为 localhost 的别名:http://auth-server,端口为 9000

@Bean
public ProviderSettings providerSettings() {
    return ProviderSettings.builder()
      .issuer("http://auth-server:9000")
      .build();
}

此外,还需要在 /etc/hosts 文件中添加一个 127.0.0.1 auth-server 条目。这样,就可以在本地计算机上运行客户端和授权服务器,避免了 Session Cookie 在两者之间覆盖的问题。

然后,使用 @EnableWebSecurity 注解配置类启用 Spring Web Security 模块:

@EnableWebSecurity
public class DefaultSecurityConfig {

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

    // ...
}

这里调用 authorizeRequests.anyRequest().authenticated() 来要求对所有请求进行身份认证。还通过调用 formLogin(defaults()) 方法提供了基于表单的身份认证。

最后,定义一组用于测试的示例用户。在本例中,创建一个只有一个 admin 用户的 Repository:

@Bean
UserDetailsService users() {
    UserDetails user = User.withDefaultPasswordEncoder()
      .username("admin")
      .password("password")
      .build();
    return new InMemoryUserDetailsManager(user);
}

3、资源服务器

现在,创建一个资源服务器,通过 GET 端点返回文章列表。这些端点应该只允许经过了授权服务器进行身份认证的请求。

3.1、依赖

首先,添加所有必要的依赖:

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

3.2、配置

application.yml 文件中配置一些属性。首先是服务器端口:

server:
  port: 8090

接下来是 Security 配置。需要使用之前在 ProviderSettings Bean 中配置的主机和端口为身份认证服务器设置正确的 URL:

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

现在,可以设置 Web 安全配置了。再次说明,对文章资源的每个请求都必须经过授权,并拥有适当的 articles.read 权限:

@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.mvcMatcher("/articles/**")
          .authorizeRequests()
          .mvcMatchers("/articles/**")
          .access("hasAuthority('SCOPE_articles.read')")
          .and()
          .oauth2ResourceServer()
          .jwt();
        return http.build();
    }
}

如上所示,调用了 oauth2ResourceServer() 方法,该方法将根据 application.yml 配置来配置与授权服务器的连接。

3.3、ArticlesController

最后,创建一个 REST Controller,通过 GET /articles 端点返回文章列表:

@RestController
@RequestMapping
public class ArticlesController {

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

4、API 客户端

最后,创建一个 REST API 客户端,从资源服务器获取文章列表。

4.1、依赖

添加所有必要的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webflux</artifactId>
    <version>5.3.9</version>
</dependency>
<dependency>
    <groupId>io.projectreactor.netty</groupId>
    <artifactId>reactor-netty</artifactId>
    <version>1.0.9</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 实例,以执行对资源服务器的 HTTP 请求。

使用标准实现,只增加一个 OAuth Authorization Filter:

@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 Security:

@EnableWebSecurity
public class SecurityConfig {

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

如上,需要对每个请求进行身份认证。此外,还需要配置登录页面的 URL(在 .yml 配置文件中定义)和 OAuth 客户端。

4.3、ArticlesController

最后,创建 Controller 访问数据。使用之前配置的 WebClient 向资源服务器发送 HTTP 请求:

@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();
    }
}

在上例中,通过 OAuth2AuthorizedClient 类从请求中获取 OAuth Authorization Token。Spring 会使用 @RegisterdOAuth2AuthorizedClient 注解自动将其绑定,并进行适当的标识。在本例中,它来自之前在 .yml 文件中配置的 article-client-authorizaiton-code

该 Authorization Token 会进一步传递给 HTTP 请求。

4.4、访问文章列表

打开浏览器并尝试访问 http://127.0.0.1:8080/articles 页面时,会被自动重定向到 http://auth-server:9000/login URL 下的授权服务器登录页面:

登录页

提供正确的用户名和密码后,授权服务器会将我们重定向到请求的 URL,即文章列表。

进一步对文章端点的请求无需登录,因为 Access Token 将保存在 cookie 中。

5、总结

本文介绍了如何设置、配置和使用 Spring Security OAuth 授权服务器。


Ref:https://www.baeldung.com/spring-security-oauth-auth-server