SpringSecurity整合OAuth2授权的高阶配置

1,138 阅读7分钟

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

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

  • 授权端点: 由客户端使用,通过用户代理重定向从资源所有者获得授权。
  • 令牌端点: 由客户端使用,用于交换认证授权给访问令牌,通常与客户端身份验证一起使用。

以及一个客户端端点:

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

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

UserInfo端点是一个OAuth 2.0保护资源,它返回关于经过身份验证的最终用户的声明。为了获得关于最终用户的请求,客户端使用通过OpenID连接身份验证获得的访问令牌向UserInfo端点发出请求。这些声明通常由一个JSON对象表示,该对象包含声明的名称-值对集合。

ServerHttpSecurity.oauth2Login()提供了许多自定义OAuth 2.0登录的配置选项。

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

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.oauth2Login(oauth2 -> oauth2
                .authenticationConverter(this.authenticationConverter())
                .authenticationMatcher(this.authenticationMatcher())
                .authenticationManager(this.authenticationManager())
                .authenticationSuccessHandler(this.authenticationSuccessHandler())
                .authenticationFailureHandler(this.authenticationFailureHandler())
                .clientRegistrationRepository(this.clientRegistrationRepository())
                .authorizedClientRepository(this.authorizedClientRepository())
                .authorizedClientService(this.authorizedClientService())
                .authorizationRequestResolver(this.authorizationRequestResolver())
                .authorizationRequestRepository(this.authorizationRequestRepository())
                .securityContextRepository(this.securityContextRepository())
            );
​
        return http.build();
    }
}

下面的章节将详细介绍每个可用的配置选项:

OAuth 2.0登录页面

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

为了让LoginPageGeneratingWebFilter显示配置的OAuth客户端的链接,注册的ReactiveClientRegistrationRepository还需要实现Iterable<ClientRegistration>。参考InMemoryReactiveClientRegistrationRepository

每个OAuth客户端链接的目的地默认为: "/oauth2/authorization/{registrationId}"

要覆盖默认登录页面,请配置exceptionHandling().authenticationEntryPoint()和(可选的)oauth2Login().authorizationRequestResolver()

下面的清单显示了一个示例:

package com.hz.ss.config;
​
import org.springframework.context.annotation.Bean;
...
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
​
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2")))
            .oauth2Login(oauth2 -> oauth2
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                    .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}")));
​
        return http.build();
    }
​
    private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
        ServerWebExchangeMatcher authorizationRequestMatcher = new PathPatternParserServerWebExchangeMatcher("/login/oauth2/authorization/{registrationId}");
​
        return new DefaultServerOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository(), authorizationRequestMatcher);
    }
    
    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryReactiveClientRegistrationRepository(this.giteeClientRegistration());
    }
​
    /**
     * 上一项目使用 application.properties 配置方式 这里我们使用编程方式来实现自定义OAuth提供者
     */
    private ClientRegistration giteeClientRegistration() {
        return ClientRegistration.withRegistrationId("gitee")
                .clientId("26511dbc353a90b647fbab9e24341d3a36a5bd7cf261b846c41fbd4e8500dcdf")
                .clientSecret("ae4a6d5697e962c1b4f8043ba74b99aa58e930143758abcf69c3d2f1a209ca47")
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
                //.scope("openid", "profile", "email", "address", "phone")
                .authorizationUri("https://gitee.com/oauth/authorize")
                .tokenUri("https://gitee.com/oauth/token")
                .userInfoUri("https://gitee.com/api/v5/user")
                .userNameAttributeName(IdTokenClaimNames.SUB)
                .clientName("Gitee")
                .build();
    }
}
​

重要: 您需要使用@RequestMapping("/login/oauth2")来提供能够呈现自定义登录页面的@Controller

Tip: 如前所述,配置oauth2Login().authorizationRequestResolver()是可选的。但是,如果您选择定制它,请确保到每个OAuth Client的链接与通过ServerWebExchangeMatcher提供的模式匹配。例:上一节的/login/oauth/authorization/gitee就需要改为/login/oauth2/authorization/gitee

重定向端点

授权服务器使用重定向端点通过资源所有者用户代理将授权响应(其中包含授权凭据)返回给客户端。

Tip:OAuth 2.0登录利用授权码授权。因此,授权凭证就是授权代码。

默认的授权响应重定向端点为/login/oauth2/code/{registrationId}

如果您想自定义授权响应重定向端点,请按照如下示例配置它:

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.
            ...
            oauth2Login(oauth2 -> oauth2.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}"))
            );
​
        return http.build();
    }
}

在上面代码的基础上我们将/login/oauth2/code/{registrationId}改为我们自定义的重定向URL/login/oauth2/callback/{registrationId},也是可以正常授权的。

重要:您还需要确保ClientRegistration.redirectUri匹配自定义授权响应重定向端点。

用户信息端点

UserInfo端点包括许多配置选项,如下面的子部分所述:

映射用户权限

在用户成功地通过OAuth 2.0 提供者进行身份验证后,OAuth2User.getAuthorities()(或OidcUser.getAuthorities())可能会映射到一组新的GrantedAuthority实例,当完成身份验证时,这些实例将被提供给OAuth2AuthenticationToken

Tip:OAuth2AuthenticationToken.getAuthorities()用于授权请求,例如hasRole('USER')hasRole('ADMIN')

当映射用户权限时,有两个选项可以选择:

  • 使用GrantedAuthoritiesMapper [一]
  • 基于ReactiveOAuth2UserService的委派策略 [二]

、注册一个GrantedAuthoritiesMapper @Bean,让它自动应用到配置中,如下面的示例所示:

package com.hz.ss.config;
​
...
​
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    // 自定义重定向,而不使用默认的 /login/oauth2/code/{registrationId}
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        ...
            .oauth2Login(oauth2 -> oauth2
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                    .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}"))
            );
​
        return http.build();
    }
​
    private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
        ...
    }
​
    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        ...
    }
​
    /**
     * 上一项目使用 application.properties 配置方式 这里我们使用编程方式来实现自定义OAuth提供者
     */
    private ClientRegistration giteeClientRegistration() {
        return ClientRegistration.withRegistrationId("gitee")
             ...
            // 这里的重定向地址我们不再使用默认的 URL 默认为/login/oauth2/code/{registrationId}
                .redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
             ...
                .build();
    }
​
    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
​
            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
​
                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
​
                    // 将在idToken和/或userInfo中发现的声明映射到一个或多个GrantedAuthority,并将其添加到 mappedAuthorities
​
                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
​
                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
​
                    // 将userAttributes中的属性映射到一个或多个GrantedAuthority,并将其添加到mappedAuthorities
​
                }
            });
​
            return mappedAuthorities;
        };
    }
}
​

、与使用GrantedAuthortiesMapper相比,这种策略是高级的,但是,它也更灵活,因为它允许您访问OAuth2UserRequestOAuth2User(当使用OAuth 2.0 UserService时)或OidcUserRequestOidcUser(当使用OpenID Connect 1.0 UserService时)。

OAuth2UserRequest(和OidcUserRequest)为您提供了对相关联的OAuth2AccessToken的访问,当委托程序在映射用户的自定义权限之前需要从受保护的资源中获取权限信息时,这非常有用。

下面的例子展示了如何使用OpenID Connect 1.0 UserService实现和配置基于委托的策略:

@Bean
    public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
​
        return (userRequest) -> {
            // 委托给默认实现以加载用户
            return delegate.loadUser(userRequest)
                    .flatMap((oidcUser) -> {
                        OAuth2AccessToken accessToken = userRequest.getAccessToken();
                        Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
​
                        // 1) 使用accessToken从受保护的资源获取权限信息
                        // 2) 将权限信息映射到一个或多个GrantedAuthority,并将其添加到 mappedAuthority
                        // 3) 创建oidcUser的副本,但使用mappedAuthorities代替
                        oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
​
                        return Mono.just(oidcUser);
                    });
        };
    }
OAuth 2.0 用户服务

DefaultReactiveOAuth2UserServiceReactiveOAuth2UserService的一个实现,它支持标准的OAuth 2.0 Provider。

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

DefaultReactiveOAuth2UserService在UserInfo端点请求用户属性时使用WebClient

如果你需要自定义UserInfo Request的预处理[和/或]UserInfo Response的后处理,你需要提供DefaultReactiveOAuth2UserService.setWebClient()和一个自定义配置的WebClient

无论你是自定义DefaultReactiveOAuth2UserService还是提供你自己的ReactiveOAuth2UserService实现,你都需要配置它,如下面的例子所示:

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            ...
            .oauth2Login(withDefaults());
​
        return http.build();
    }
​
    @Bean
    public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        ...
    }
}
OpenID Connect 1.0 用户服务

OidcReactiveOAuth2UserService是一个ReactiveOAuth2UserService的实现,它支持OpenID Connect 1.0 Provider。

当请求UserInfo端点上的用户属性时,OidcReactiveOAuth2UserService利用DefaultReactiveOAuth2UserService

如果你需要定制UserInfo请求的预处理和/或UserInfo响应的事后处理,你需要提供一个自定义配置的ReactiveOAuth2UserService.setoauth2userservice()ReactiveOAuth2UserService

无论你是定制OidcReactiveOAuth2UserService还是为OpenID Connect 1.0 Provider’s提供你自己的ReactiveOAuth2UserService实现,你都需要配置它,如下所示的示例:

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
​
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            ...
            .oauth2Login(withDefaults());
​
        return http.build();
    }
​
    @Bean
    public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        ...
    }
}
ID令牌签名验证

OpenID Connect 1.0认证引入了ID Token,这是一个安全令牌,包含了当客户端使用时关于授权服务器对终端用户进行认证的声明。

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

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

JWS算法解析器是一个接受ClientRegistration并为客户端返回预期JwsAlgorithmFunction。例如:SignatureAlgorithm.RS256MacAlgorithm.HS256

下面的代码展示了如何将OidcIdTokenDecoderFactory @Bean配置所有ClientRegistration为默认的MacAlgorithm.HS256:

@Bean
public ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
    idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
    return idTokenDecoderFactory;
}

Note:对于基于MAC的算法,如HS256HS384HS512,使用与client-id对应的client-secret作为签名验证的对称密钥。

Tip:如果OpenID Connect 1.0认证配置了多个ClientRegistration, JWS算法解析器可能会评估提供的ClientRegistration,以确定返回哪种算法。

OpenID Connect 1.0注销

OpenID Connect Session Management 1.0允许使用客户端注销提供商的终端用户。可用的策略之一是RP-Initiated Logout

如果OpenID提供者同时支持会话管理和发现,客户端可以从OpenID提供者的发现元数据中获取end_session_endpoint URL。这可以通过使用issuer-uri配置ClientRegistration来实现,如下所示:

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            ...
        provider:
          okta:
            issuer-uri: https://dev-1234.oktapreview.com

\