当前位置: 首页 > 工具软件 > OAuth AS3 > 使用案例 >

Spring Security / Servlet Application /42 OAuth 2.0 / OAuth 2.0 Client

微生德泽
2023-12-01

样例代码:Spring Security Sample

参考:Spring Security Reference


Spring Security 的 OAuth 2.0 Client 特性为 OAuth 2.0 鉴权框架 中定义的
Client 角色提供了支持

高层的核心特性有:

Authorization Grant 支持

在 OAuth 2.0 中,Authorization Grant 表示授权码类型
授权码是一个用来换取 Access Token 的 code,它代表着资源所有者对三方系统的授权
OAuth 2.0 支持多种类型的授权码,并且还支持扩展自定义的授权码类型
其中一种扩展,就是集成 Assertion 框架
将其中的 Assertion 作为授权码
而 Assertion 的具体实现又需要其它的框架支持,例如 SAMLJWT

另外,OAuth 2.0 还包括 Client 鉴权,即授权服务器认证三方系统身份的流程(可选的)
这个流程也支持 JWT 扩展

Client 鉴权支持

HTTP 客户端支持

Servlet 环境的 WebClient 集成,就是一个 HTTP 客户端,用来发送 HTTP 请求
集成后,可以在 Client 后端发起访问资源服务的 HTTP 请求时使用

HttpSecurity.oauth2Client() DSL 概览:

@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2ClientConfigurer -> oauth2ClientConfigurer
                .clientRegistrationRepository(clientRegistrationRepository())
                .authorizedClientRepository(authorizedClientRepository())
                .authorizedClientService(authorizedClientService())
                .authorizationCodeGrant(codeGrant -> codeGrant
                    .authorizationRequestRepository(authorizationRequestRepository())
                    .authorizationRequestResolver(authorizationRequestResolver())
                    .accessTokenResponseClient(accessTokenResponseClient())
                )
            );
    }

}

1. 核心接口 / 类

ClientRegistration

ClientRegistration 代表一个注册到 OAuth 2.0 或 OpenID Connect 1.0 提供者(Provider)的 Client

一个 ClientRegistration 中保存了关于 Client 的信息:

package org.springframework.security.oauth2.client.registration;

public final class ClientRegistration {
    
    private String registrationId; // ClientRegistration 的唯一标识
    private String clientId; // OAuth2 标准中的 Client ID,向授权服务器标识 Client 的身份
    private String clientSecret; // OAuth2 标准中的 Client Secret
  
    /*
    请求供应商(Provider)认证 Client 身份时使用的方法,支持的方法有:
    client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt, none
     */
    private ClientAuthenticationMethod clientAuthenticationMethod;
    
    /*
    OAuth 2.0 授权框架定义的授权码类型,支持的类型有:
    authorization_code, client_credentials, password, as well as, urn:ietf:params:oauth:grant-type:jwt-bearer(扩展的类型)
     */
    private AuthorizationGrantType authorizationGrantType;
    
    /*
    Client 注册到授权中心的重定向 URI
    当终端用户鉴权成功,并为 Client 授权访问之后,授权中心会让用户代理(如:浏览器)重定向到这个 URI
     */
    private String redirectUri;
    
    private Set<String> scopes = Collections.emptySet(); // Client 请求的 scope(权限)如:openid, email, or profile
    private ProviderDetails providerDetails = new ProviderDetails();
    private String clientName; // Client 的一个描述性的名称,例如在登录页上展示三方登录的名称

    public class ProviderDetails {
        
        private String authorizationUri; // 授权服务器的授权接口 URI(Authorization Endpoint)
        private String tokenUri; // 授权服务器用于通过授权码换取 Access Token 的接口(Token Endpoint)
        private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint();
        
        /*
        从授权服务器查询 JSON Web Key (JWK) Set 的接口
        JWK 中包含用于验证 ID Token(OpenID Connect 会用到) 的 JSON Web Signature (JWS) 的密钥  
        也可以用于验证 UserInfo 响应(从资源服务器获取的用户信息)
         */
        private String jwkSetUri;

        // 返回 OpenID Connect 1.0 提供者(Provider)或 OAuth 2.0 授权服务器的发行者标识
        private String issuerUri;

        /*
        OpenID Provider Configuration Information
        这个信息只有在 Spring Boot 2.x 配置了 spring.security.oauth2.client.provider.[providerId].issuerUri 属性时可用
         */
        private Map<String, Object> configurationMetadata = Collections.emptyMap();

        public class UserInfoEndpoint {

            // 向资源服务器查询终端用户信息的 URI(UserInfo Endpoint)
            private String uri;
            
            // 用 Access Token 访问 UserInfo Endpoint 获取用户信息时使用的的方法
            private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
            
            // 访问 UserInfo Endpoint 后返回的用户信息响应(UserInfo Response)中,用户名的参数名称(用户名泛指唯一标识终端用户的信息)
            private String userNameAttributeName;   

        }
        
    }

}

参考:JSON Web Key (JWK)JSON Web Signature (JWS)OpenID Provider Configuration Information

一个 ClientRegistration 可以通过下边两种发现机制进行配置

  1. OpenID Connect 提供者(Provider)的 Configuration endpoint
  2. 授权服务器的 Metadata endpoint

ClientRegistrations 提供了通过这种发现机制来配置 ClientRegistration 的方法:

@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {

    ClientRegistration clientRegistration() {
        return ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
    }
  
}

上边的代码会依次访问下边的 URL 直到获得 200 状态码:

  1. https://idp.example.com/issuer/.well-known/openid-configuration
  2. https://idp.example.com/.well-known/openid-configuration/issuer
  3. https://idp.example.com/.well-known/oauth-authorization-server/issuer

使用第一个 200 状态码的响应来配置 ClientRegistration

也可以使用 ClientRegistrations.fromOidcIssuerLocation(String issuer)
这个方法只会访问 OpenID Connect 提供者(Provider)的 Configuration endpoint

ClientRegistrationRepository

ClientRegistrationRepository 作为存储 OAuth 2.0 / OpenID Connect 1.0 的 ClientRegistration 的仓库

Client 注册信息由相关的授权服务器所有,最终也是以存储在授权服务器的信息为准
ClientRegistrationRepository 存储的是完整 Client 注册信息的一个子集,并且需要与授权服务器存储的注册信息保持一致
ClientRegistrationRepository 是否通过授权服务器提供的接口同步 Client 注册信息,以及如何同步取决于具体实现

ClientRegistrationRepository 的默认实现是 InMemoryClientRegistrationRepository
用一个 Map<String, ClientRegistration> 存储 registrationId -> ClientRegistration 映射

Spring Boot 2.x 的自动配置会将 spring.security.oauth2.client.registration.[registrationId] 下的属性绑定到 ClientRegistration 实例
然后将这些 ClientRegistration 存入 ClientRegistrationRepository

自动配置还会将 ClientRegistrationRepository 注册为 ApplicationContext 中的 @Bean,在应用中,可以通过依赖注入获取

@RestController
@RequestMapping("test")
public class TestController {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("foo/registration")
    public String fooRegistration() {
    ClientRegistration fooRegistration = clientRegistrationRepository.findByRegistrationId("foo");
        return fooRegistration == null ? "fooRegistration == null" : fooRegistration.toString();
    }

}

OAuth2AuthorizedClient

OAuth2AuthorizedClient 代表一个已被授权的 Client
当终端用户(资源拥有者)向某个 Client 授予了访问受保护资源的权限时,这个 Client 被认为是已被授权的 Client

OAuth2AuthorizedClientOAuth2AccessToken(可选的 OAuth2RefreshToken)和 ClientRegistration 与资源拥有者关联起来
其中的 Principal 指的是终端用户(资源拥有者)

OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository 负责在 Web 请求之间持久化 OAuth2AuthorizedClient(通常对应会话域)

从开发者的角度来看,OAuth2AuthorizedClientRepository 或者 OAuth2AuthorizedClientService 提供了查找某个 Client 对应的 OAuth2AccessToken 的方法
在向资源服务器发起访问用户资源的请求时,必须要先获取 OAuth2AccessToken

@RestController
@RequestMapping("test")
public class TestController {

    @Autowired
    private OAuth2AuthorizedClientService oauth2AuthorizedClientService;

    // ...
  
    @GetMapping("foo/accesstoken")
    public String fooAccessToken(Authentication authentication) {
        OAuth2AuthorizedClient authorizedClient = oauth2AuthorizedClientService
           .loadAuthorizedClient("foo", authentication.getName());
        return authorizedClient.getAccessToken().getTokenType().getValue();
    }

}

Spring Boot 2.x 的自动配置在 ApplicationContext 注册了 OAuth2AuthorizedClientRepository 和/或 OAuth2AuthorizedClientService @Bean
客户端程序员也可以注册自己的 OAuth2AuthorizedClientRepositoryOAuth2AuthorizedClientService @Bean

OAuth2AuthorizedClientService 的默认实现是 InMemoryOAuth2AuthorizedClientService
在内存中存储 OAuth2AuthorizedClient

可以配置 JdbcOAuth2AuthorizedClientService 通过 JDBC 将 OAuth2AuthorizedClient 持久化到数据库

JdbcOAuth2AuthorizedClientService 依赖 OAuth 2.0 Client Schema 中的数据库表结构

OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider

OAuth2AuthorizedClientManager 负责对 OAuth2AuthorizedClient 进行管理

其主要责任有:

  1. 使用 OAuth2AuthorizedClientProvider 为 OAuth 2.0 Client 授权(或重新授权)
  2. 委托 OAuth2AuthorizedClientServiceOAuth2AuthorizedClientRepositoryOAuth2AuthorizedClient 进行持久化
  3. 当一个 OAuth 2.0 Client 被成功授权(或重新授权)后,委托 OAuth2AuthorizationSuccessHandler 进行处理
  4. 当一个 OAuth 2.0 Client 授权(或重新授权)失败后,委托 OAuth2AuthorizationFailureHandler 进行处理

OAuth2AuthorizedClientProvider 会实现一个为 OAuth 2.0 Client 授权(或重新授权)的策略
一般会实现一种授权码类型(Authorization Grant),例如:authorization_code、client_credentials 等

OAuth2AuthorizedClientManager 的默认实现是 DefaultOAuth2AuthorizedClientManager
这个实现关联着一个基于委派组合的支持多种授权码类型的 OAuth2AuthorizedClientProvider
这个实现是用 OAuth2AuthorizedClientProviderBuilder 构建的 DelegatingOAuth2AuthorizedClientProvider 对象
客户端程序员也可以使用 OAuth2AuthorizedClientProviderBuilder 配置并构建一个基于委派组合的 DelegatingOAuth2AuthorizedClientProvider

下边的例子展示了如何用 OAuth2AuthorizedClientProviderBuilder 配置并构建一个 DelegatingOAuth2AuthorizedClientProvider
支持:authorization_code、refresh_token、client_credentials、基于密码的授权

@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {

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

        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .authorizationCode()
            .refreshToken()
            .clientCredentials()
            .password()
            .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

授权成功后,DefaultOAuth2AuthorizedClientManager 会调用 OAuth2AuthorizationSuccessHandler.onAuthorizationSuccess()
OAuth2AuthorizationSuccessHandler 默认会通过 OAuth2AuthorizedClientRepository 保存 OAuth2AuthorizedClient

重新授权失败后,例如 Refresh Token 失效了
RemoveAuthorizedClientOAuth2AuthorizationFailureHandler 会将之前保存的 OAuth2AuthorizedClientOAuth2AuthorizedClientRepository 中移除
可以通过 setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler) 配置自定义处理器

DefaultOAuth2AuthorizedClientManager 持有一个 contextAttributesMapper,类型为 Function<OAuth2AuthorizeRequest, Map<String, Object>>
负责将 OAuth2AuthorizeRequest 中的属性放入一个属性值的 Map,这个 Map 的值会被加入到 OAuth2AuthorizationContext
某些 OAuth2AuthorizedClientProvider 需要用到请求中的参数,这时就可以通过这种方式获取到
例如 PasswordOAuth2AuthorizedClientProvider 需要从 OAuth2AuthorizationContext.getAttributes() 中获取资源拥有者的用户名和密码

contextAttributesMapper 这个函数式提供了一个接入点,可以自定义从请求获取需要属性的具体方式:

@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {

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

        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .password()
            .refreshToken()
            .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        /*
        假设 `用户名` 和 `密码` 由 `HttpServletRequest` 参数提供
        将 `HttpServletRequest` 参数映射到 `OAuth2AuthorizationContext.getAttributes()`
         */
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

        return authorizedClientManager;
    }

    private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
        return oauth2AuthorizeRequest -> {
            Map<String, Object> contextAttributes = Collections.emptyMap();
            HttpServletRequest servletRequest = oauth2AuthorizeRequest.getAttribute(HttpServletRequest.class.getName());
            String username = null, password = null;
            if (servletRequest != null) {
                username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
                password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
            }
            if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                contextAttributes = new HashMap<>();
                // `PasswordOAuth2AuthorizedClientProvider` 需要这两个属性
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            }
            return contextAttributes;
          };
    }
    
}

DefaultOAuth2AuthorizedClientManager 被设计为在 HttpServletRequest 上下文中使用
如果操作不在 HttpServletRequest 上下文中,则应该使用 AuthorizedClientServiceOAuth2AuthorizedClientManager

服务应用(Service Application)是一个常见的使用 AuthorizedClientServiceOAuth2AuthorizedClientManager 的场景
服务应用通常在后端运行,不与用户交互,通常使用系统级别的账户而不是普通用户账户
此时可以使用 client_credentials 类型的授权码

下边是一个通过配置 AuthorizedClientServiceOAuth2AuthorizedClientManager 支持 client_credentials 类型授权码的例子:

@Component
public class A04_ClientCredentialOauth2ClientConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientService authorizedClientService) {

        AuthorizedClientServiceOAuth2AuthorizedClientManager
            authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientService);
        
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .build();
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

2. “ 授权码类型 ” 支持

3. “ Client 授权 ” 支持

4. 其它特性

5. Servlet 环境集成 WebClient


样例代码:Spring Security Sample

 类似资料: