51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Security Oauth 授权服务器

1、简介 {#1简介}

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

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

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

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

2、授权服务器实现 {#2授权服务器实现}

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

2.1、依赖 {#21依赖}

首先,在 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、配置 {#22配置}

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、资源服务器 {#3资源服务器}

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

3.1、依赖 {#31依赖}

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

<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、配置 {#32配置}

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 {#33articlescontroller}

最后,创建一个 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 客户端 {#4api-客户端}

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

4.1、依赖 {#41依赖}

添加所有必要的依赖:

<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、配置 {#42配置}

和前面一样,定义一些用于身份认证的配置属性:

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 {#43articlescontroller}

最后,创建 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、访问文章列表 {#44访问文章列表}

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

登录页

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

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

5、总结 {#5总结}

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


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

赞(3)
未经允许不得转载:工具盒子 » Spring Security Oauth 授权服务器