• Spring Boot OAuth 2.0整合—高级配置


    一、概述

    HttpSecurity.oauth2Login() 为定制OAuth 2.0登录提供了大量的配置选项。主要的配置选项被分组到它们的协议端点对应处。

    例如,oauth2Login().authorizationEndpoint() 允许配置授权端点,而 oauth2Login().tokenEndpoint() 允许配置令牌端点。

    下面的代码是一个例子。

    Advanced OAuth2 Login Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .authorizationEndpoint(authorization -> authorization
    9. ...
    10. )
    11. .redirectionEndpoint(redirection -> redirection
    12. ...
    13. )
    14. .tokenEndpoint(token -> token
    15. ...
    16. )
    17. .userInfoEndpoint(userInfo -> userInfo
    18. ...
    19. )
    20. );
    21. return http.build();
    22. }
    23. }

    oauth2Login() DSL的主要目标是与规范中定义的命名紧密结合。

    OAuth 2.0授权框架对 协议端点 的定义如下。

    授权过程使用两个授权服务器端点(HTTP资源)。

    • 授权端点。由客户端使用,通过用户代理重定向从资源所有者那里获得授权。
    • 令牌端点。由客户端使用,以交换授权许可的访问令牌,通常是客户端认证。

    授权过程也使用一个客户端点。

    • 重定向端点。由授权服务器使用,通过资源所有者的用户代理将包含授权凭证的响应返回给客户端。

    OpenID Connect Core 1.0 规范对 UserInfo端点 的定义如下。

    UserInfo端点是一个OAuth 2.0保护资源,用于返回关于已认证最终用户的请求。为了获得关于最终用户的请求,客户端通过使用通过OpenID连接认证获得的访问令牌向UserInfo端点提出请求。这些请求通常由一个JSON对象表示,该对象包含请求的key/value对的集合。

    下面的代码显示了 oauth2Login() DSL可用的完整配置选项。

    OAuth2 Login Configuration Options

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .clientRegistrationRepository(this.clientRegistrationRepository())
    9. .authorizedClientRepository(this.authorizedClientRepository())
    10. .authorizedClientService(this.authorizedClientService())
    11. .loginPage("/login")
    12. .authorizationEndpoint(authorization -> authorization
    13. .baseUri(this.authorizationRequestBaseUri())
    14. .authorizationRequestRepository(this.authorizationRequestRepository())
    15. .authorizationRequestResolver(this.authorizationRequestResolver())
    16. )
    17. .redirectionEndpoint(redirection -> redirection
    18. .baseUri(this.authorizationResponseBaseUri())
    19. )
    20. .tokenEndpoint(token -> token
    21. .accessTokenResponseClient(this.accessTokenResponseClient())
    22. )
    23. .userInfoEndpoint(userInfo -> userInfo
    24. .userAuthoritiesMapper(this.userAuthoritiesMapper())
    25. .userService(this.oauth2UserService())
    26. .oidcUserService(this.oidcUserService())
    27. )
    28. );
    29. return http.build();
    30. }
    31. }

    除了 oauth2Login() DSL,还支持XML配置。

    下面的代码显示了 security namespace 中可用的完整配置选项。

    OAuth2 Login XML Configuration Options

    1. <http>
    2. <oauth2-login client-registration-repository-ref="clientRegistrationRepository"
    3. authorized-client-repository-ref="authorizedClientRepository"
    4. authorized-client-service-ref="authorizedClientService"
    5. authorization-request-repository-ref="authorizationRequestRepository"
    6. authorization-request-resolver-ref="authorizationRequestResolver"
    7. access-token-response-client-ref="accessTokenResponseClient"
    8. user-authorities-mapper-ref="userAuthoritiesMapper"
    9. user-service-ref="oauth2UserService"
    10. oidc-user-service-ref="oidcUserService"
    11. login-processing-url="/login/oauth2/code/*"
    12. login-page="/login"
    13. authentication-success-handler-ref="authenticationSuccessHandler"
    14. authentication-failure-handler-ref="authenticationFailureHandler"
    15. jwt-decoder-factory-ref="jwtDecoderFactory"/>
    16. </http>

    下面几节将更详细地介绍每个可用的配置选项。

    • OAuth 2.0 登录页面
    • 重定向端点
    • UserInfo端点
    • ID Token 签名验证
    • OpenID Connect 1.0 注销

    二、OAuth 2.0 登录页面

    默认情况下,OAuth 2.0登录页面是由 DefaultLoginPageGeneratingFilter 自动生成的。默认的登录页面显示每个配置的OAuth客户端,其 ClientRegistration.clientName 为链接,能够启动授权请求(或OAuth 2.0登录)。

    对于 DefaultLoginPageGeneratingFilter 显示配置的OAuth客户端的链接,注册的 ClientRegistrationRepository 需要同时实现 Iterable。请参考 InMemoryClientRegistrationRepository。

    每个OAuth客户端的链接的目的地默认为以下内容。

    OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"

    下面一行是一个例子。

    <a href="/oauth2/authorization/google">Google</a>

    要覆盖默认的登录页面,请配置 oauth2Login().loginPage() 和(可选) oauth2Login().authorizationEndpoint().baseUri()。

    下面的列表显示了一个例子。

    OAuth2 Login Page Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .loginPage("/login/oauth2")
    9. ...
    10. .authorizationEndpoint(authorization -> authorization
    11. .baseUri("/login/oauth2/authorization")
    12. ...
    13. )
    14. );
    15. return http.build();
    16. }
    17. }

    你需要提供一个带有 @RequestMapping("/login/oauth2") 的 @Controller,能够渲染自定义的登录页面。

    如前所述,配置 oauth2Login().authorizationEndpoint().baseUri() 是可选的。但是,如果你选择自定义,请确保每个OAuth客户端的链接与 authorizationEndpoint().baseUri() 相匹配。

    下面一行显示了一个例子。

    <a href="/login/oauth2/authorization/google">Google</a>

    三、重定向端点

    重定向端点由授权服务器使用,用于通过资源所有者用户代理向客户返回授权响应(包含授权凭证)

    OAuth 2.0登录利用的是授权码授予。因此,授权凭证就是授权码。

    默认的授权响应 baseUri(重定向端点)是 /login/oauth2/code/*,这在 OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI 中定义。

    如果你想定制授权响应 baseUri,请按以下方式配置。

    Redirection Endpoint Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .redirectionEndpoint(redirection -> redirection
    9. .baseUri("/login/oauth2/callback/*")
    10. ...
    11. )
    12. );
    13. return http.build();
    14. }
    15. }

    你还需要确保 ClientRegistration.redirectUri 与自定义授权响应 baseUri 相匹配。

    下面的列表显示了一个例子。

    • Java
    1. return CommonOAuth2Provider.GOOGLE.getBuilder("google")
    2. .clientId("google-client-id")
    3. .clientSecret("google-client-secret")
    4. .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
    5. .build();

    四、UserInfo端点

    UserInfo端点包括一些配置选项,在下面的小节中描述。

    • 映射用户权限
    • OAuth 2.0 UserService
    • OpenID Connect 1.0 UserService

    1、映射用户权限

    在用户成功通过 OAuth 2.0 提供商的认证后,OAuth2User.getAuthorities()(或 OidcUser.getAuthorities())包含一个由 OAuth2UserRequest.getAccessToken().getScopes() 填充的授予权限列表,其前缀为 SCOPE_。这些授权可以被映射到一组新的 GrantedAuthority 实例中,在完成认证时提供给 OAuth2AuthenticationToken。

    OAuth2AuthenticationToken.getAuthorities() 用于授权请求,如 hasRole('USER') 或 hasRole('ADMIN') 中。

    在映射用户权限时,有几个选项可以选择。

    • 使用 GrantedAuthoritiesMapper
    • 基于Delegation的策略与OAuth2UserService

    2、使用 GrantedAuthoritiesMapper

    GrantedAuthoritiesMapper 被赋予一个授予权限的列表,其中包含一个类型为 OAuth2UserAuthority 和权限字符串 OAUTH2_USER(或 OidcUserAuthority 和权限字符串 OIDC_USER)的特殊权限。

    提供一个 GrantedAuthoritiesMapper 的实现并对其进行配置,如下所示。

    Granted Authorities Mapper Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .userInfoEndpoint(userInfo -> userInfo
    9. .userAuthoritiesMapper(this.userAuthoritiesMapper())
    10. ...
    11. )
    12. );
    13. return http.build();
    14. }
    15. private GrantedAuthoritiesMapper userAuthoritiesMapper() {
    16. return (authorities) -> {
    17. Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    18. authorities.forEach(authority -> {
    19. if (OidcUserAuthority.class.isInstance(authority)) {
    20. OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
    21. OidcIdToken idToken = oidcUserAuthority.getIdToken();
    22. OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
    23. // Map the claims found in idToken and/or userInfo
    24. // to one or more GrantedAuthority's and add it to mappedAuthorities
    25. } else if (OAuth2UserAuthority.class.isInstance(authority)) {
    26. OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
    27. Map userAttributes = oauth2UserAuthority.getAttributes();
    28. // Map the attributes found in userAttributes
    29. // to one or more GrantedAuthority's and add it to mappedAuthorities
    30. }
    31. });
    32. return mappedAuthorities;
    33. };
    34. }
    35. }

    或者,你可以注册一个 GrantedAuthoritiesMapper @Bean,让它自动应用于配置,如下所示。

    Granted Authorities Mapper Bean Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(withDefaults());
    8. return http.build();
    9. }
    10. @Bean
    11. public GrantedAuthoritiesMapper userAuthoritiesMapper() {
    12. ...
    13. }
    14. }

    3、基于Delegation的策略与OAuth2UserService

    与使用 GrantedAuthoritiesMapper 相比,这种策略是先进的。然而,它也更灵活,因为它让你可以访问 OAuth2UserRequest 和 OAuth2User(当使用OAuth 2.0 UserService)或 OidcUserRequest 和 OidcUser(当使用 OpenID Connect 1.0 UserService)。

    OAuth2UserRequest(和 OidcUserRequest)为你提供了对相关 OAuth2AccessToken 的访问,这在 delegator 需要从受保护的资源中获取权限信息,然后再为用户映射自定义权限的情况下非常有用。

    下面的例子显示了如何使用OpenID Connect 1.0 UserService实现和配置基于delegation的策略。

    OAuth2UserService Configuration

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .userInfoEndpoint(userInfo -> userInfo
    9. .oidcUserService(this.oidcUserService())
    10. ...
    11. )
    12. );
    13. return http.build();
    14. }
    15. private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    16. final OidcUserService delegate = new OidcUserService();
    17. return (userRequest) -> {
    18. // Delegate to the default implementation for loading a user
    19. OidcUser oidcUser = delegate.loadUser(userRequest);
    20. OAuth2AccessToken accessToken = userRequest.getAccessToken();
    21. Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    22. // TODO
    23. // 1) Fetch the authority information from the protected resource using accessToken
    24. // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
    25. // 3) Create a copy of oidcUser but use the mappedAuthorities instead
    26. oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
    27. return oidcUser;
    28. };
    29. }
    30. }

    4、OAuth 2.0 UserService

    DefaultOAuth2UserService 是一个 OAuth2UserService 的实现,支持标准的OAuth2.0提供者。

    OAuth2UserService 从 UserInfo 端点获取最终用户(资源所有者)的用户属性(通过使用授权流程中授予客户端的访问令牌),并以 OAuth2User 的形式返回一个 AuthenticatedPrincipal。

    DefaultOAuth2UserService 在UserInfo端点请求用户属性时使用 RestOperations 实例。

    如果你需要定制UserInfo请求的预处理,你可以为 DefaultOAuth2UserService.setRequestEntityConverter() 提供一个自定义的 Converter>。默认的实现 OAuth2UserRequestEntityConverter 建立了一个UserInfo请求的 RequestEntity 表示,默认在授权header中设置 OAuth2AccessToken。

    在另一端,如果你需要定制 UserInfo 响应的后处理,你需要为 DefaultOAuth2UserService.setRestOperations() 提供一个自定义配置的 RestOperations。默认的 RestOperations 配置如下。

    1. RestTemplate restTemplate = new RestTemplate();
    2. restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

    OAuth2ErrorResponseErrorHandler 是一个 ResponseErrorHandler,可以处理OAuth2.0错误(400 Bad Request)。它使用一个 OAuth2ErrorHttpMessageConverter 将 OAuth2.0 错误参数转换为 OAuth2Error。

    无论你是定制 DefaultOAuth2UserService 还是提供你自己的 OAuth2UserService 的实现,你都需要按以下方式进行配置。

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .userInfoEndpoint(userInfo -> userInfo
    9. .userService(this.oauth2UserService())
    10. ...
    11. )
    12. );
    13. return http.build();
    14. }
    15. private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
    16. ...
    17. }
    18. }

    5、OpenID Connect 1.0 UserService

    OidcUserService 是一个 OAuth2UserService 的实现,支持OpenID Connect 1.0提供者。

    OidcUserService 在 UserInfo 端点请求用户属性时利用了 DefaultOAuth2UserService。

    如果你需要定制UserInfo请求的预处理或UserInfo响应的后处理,你需要向 OidcUserService.setOauth2UserService() 提供一个自定义配置的 DefaultOAuth2UserService。

    无论你是定制 OidcUserService 还是为 OpenID Connect 1.0 提供商提供自己的 OAuth2UserService 实现,你都需要按以下方式配置它。

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Bean
    5. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    6. http
    7. .oauth2Login(oauth2 -> oauth2
    8. .userInfoEndpoint(userInfo -> userInfo
    9. .oidcUserService(this.oidcUserService())
    10. ...
    11. )
    12. );
    13. return http.build();
    14. }
    15. private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    16. ...
    17. }
    18. }

    五、ID Token 签名验证

    OpenID Connect 1.0认证引入了 ID Token,它是一个安全令牌,包含授权服务器在客户端使用时对终端用户进行认证的要求。

    ID令牌被表示为 JSON Web Token(JWT),并且必须使用 JSON Web Signature (JWS)进行签名。

    OidcIdTokenDecoderFactory 提供一个用于 OidcIdToken 签名验证的 JwtDecoder。默认算法是 RS256,但在客户端注册时分配的算法可能不同。对于这些情况,你可以配置解析器以返回为特定客户分配的预期 JWS 算法。

    JWS算法解析器是一个 Function,它接受 ClientRegistration 并返回客户端预期的 JwsAlgorithm,例如 SignatureAlgorithm.RS256 或 MacAlgorithm.HS256。

    下面的代码显示了如何配置 OidcIdTokenDecoderFactory @Bean 为所有 ClientRegistration 实例默认为 MacAlgorithm.HS256。

    • Java
    1. @Bean
    2. public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    3. OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
    4. idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
    5. return idTokenDecoderFactory;
    6. }

    对于基于MAC的算法(如 HS256、HS384 或 HS512),与 client-id 对应的 client-secret 被用作签名验证的对称密钥。

    如果为 OpenID Connect 1.0 认证配置了一个以上的 ClientRegistration,JWS 算法解析器可以评估所提供的 ClientRegistration 来决定返回哪种算法。

    六、OpenID Connect 1.0 注销

    OpenID Connect会话管理1.0允许通过使用客户端在提供方注销终端用户的能力。其中一个可用的策略是 RP发起的注销。

    如果OpenID提供商同时支持会话管理和 Discovery,客户端可以从OpenID提供商的 Discovery Metadata 中获得 end_session_endpoint URL。你可以通过在 ClientRegistration 中配置 issuer-uri 来做到这一点,如下所示。

    1. spring:
    2. security:
    3. oauth2:
    4. client:
    5. registration:
    6. okta:
    7. client-id: okta-client-id
    8. client-secret: okta-client-secret
    9. ...
    10. provider:
    11. okta:
    12. issuer-uri: https://dev-1234.oktapreview.com

    另外,你可以配置 OidcClientInitiatedLogoutSuccessHandler,它实现了RP发起的注销,如下所示。

    • Java
    1. @Configuration
    2. @EnableWebSecurity
    3. public class OAuth2LoginSecurityConfig {
    4. @Autowired
    5. private ClientRegistrationRepository clientRegistrationRepository;
    6. @Bean
    7. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    8. http
    9. .authorizeHttpRequests(authorize -> authorize
    10. .anyRequest().authenticated()
    11. )
    12. .oauth2Login(withDefaults())
    13. .logout(logout -> logout
    14. .logoutSuccessHandler(oidcLogoutSuccessHandler())
    15. );
    16. return http.build();
    17. }
    18. private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    19. OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
    20. new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
    21. // Sets the location that the End-User's User Agent will be redirected to
    22. // after the logout has been performed at the Provider
    23. oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
    24. return oidcLogoutSuccessHandler;
    25. }
    26. }

    OidcClientInitiatedLogoutSuccessHandler 支持 {baseUrl} 占位符。 如果使用,应用程序的基本URL,如 app.example.org,将在请求时替换它。

  • 相关阅读:
    Zookeeper基础
    Pytest系列-测试用例前后置固件setup和teardown的介绍和使用(2)
    【产业前沿】树莓集团如何以数字媒体产业园为引擎,加速产业升级?
    C++中静态成员变量和普通成员变量、私有成员变量和公有成员变量的区别
    深入浅出Spring注解(22)
    力扣刷题-数组-二分查找总结
    适用于物联网数据共享的区块链节点存储优化方案
    11.Spring security跨域问题
    NetSuite 关闭期间的销售订单可否修改
    TiDB v6.0.0 (DMR) :缓存表初试丨TiDB Book Rush
  • 原文地址:https://blog.csdn.net/leesinbad/article/details/133940522