高级配置

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

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

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

  • 授权端点:由客户端使用,通过用户代理重定向从资源所有者那里获得授权。

  • 令牌端点:客户端用于交换授权许可,以获得访问令牌,通常有客户端认证。

以及一个客户端端点。

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

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

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

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

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

OAuth2 Login Configuration Options
  • Java

  • Kotlin

@Configuration
@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();
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login {
                authenticationConverter = authenticationConverter()
                authenticationMatcher = authenticationMatcher()
                authenticationManager = authenticationManager()
                authenticationSuccessHandler = authenticationSuccessHandler()
                authenticationFailureHandler = authenticationFailureHandler()
                clientRegistrationRepository = clientRegistrationRepository()
                authorizedClientRepository = authorizedClientRepository()
                authorizedClientService = authorizedClientService()
                authorizationRequestResolver = authorizationRequestResolver()
                authorizationRequestRepository = authorizationRequestRepository()
                securityContextRepository = 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}"

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

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

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

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

OAuth2 Login Page Configuration
  • Java

  • Kotlin

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

		return http.build();
	}

	private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() {
		ServerWebExchangeMatcher authorizationRequestMatcher =
				new PathPatternParserServerWebExchangeMatcher(
						"/login/oauth2/authorization/{registrationId}");

		return new DefaultServerOAuth2AuthorizationRequestResolver(
				this.clientRegistrationRepository(), authorizationRequestMatcher);
	}

	...
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            exceptionHandling {
                authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2")
            }
            oauth2Login {
                authorizationRequestResolver = authorizationRequestResolver()
            }
        }

        return http.build()
    }

    private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver {
        val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher(
            "/login/oauth2/authorization/{registrationId}"
        )

        return DefaultServerOAuth2AuthorizationRequestResolver(
            clientRegistrationRepository(), authorizationRequestMatcher
        )
    }

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

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

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

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

重定向端点

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

OAuth 2.0登录利用的是授权码授予(Authorization Code Grant)。因此,授权凭证(authorization credential)就是授权码(authorization code)。

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

如果你想定制授权响应重定向端点,请按照下面的例子进行配置。

Redirection Endpoint Configuration
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

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

		return http.build();
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login {
                authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")
            }
        }

        return http.build()
    }
}

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

下面列出了一个例子。

  • Java

  • Kotlin

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
	.clientId("google-client-id")
	.clientSecret("google-client-secret")
	.redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
	.build();
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
    .clientId("google-client-id")
    .clientSecret("google-client-secret")
    .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
    .build()

UserInfo 端点

UserInfo端点包括一些配置选项,如以下各小节所述。

映射用户权限

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

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

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

使用 GrantedAuthoritiesMapper

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

注册一个 GrantedAuthoritiesMapper @Bean,让它自动应用于配置,如下例所示。

Granted Authorities Mapper Configuration
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.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();

					// Map the claims found in idToken and/or userInfo
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				} else if (OAuth2UserAuthority.class.isInstance(authority)) {
					OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

					Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

					// Map the attributes found in userAttributes
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				}
			});

			return mappedAuthorities;
		};
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority> ->
        val mappedAuthorities = emptySet<GrantedAuthority>()

        authorities.forEach { authority ->
            if (authority is OidcUserAuthority) {
                val idToken = authority.idToken
                val userInfo = authority.userInfo
                // Map the claims found in idToken and/or userInfo
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            } else if (authority is OAuth2UserAuthority) {
                val userAttributes = authority.attributes
                // Map the attributes found in userAttributes
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            }
        }

        mappedAuthorities
    }
}

使用 ReactiveOAuth2UserService 的基于委托的策略

与使用 GrantedAuthoritiesMapper 相比,这个策略是先进的,然而,它也更灵活,因为它让你访问 OAuth2UserRequestOAuth2User(当使用 OAuth 2.0 UserService 时)或 OidcUserRequestOidcUser(当使用 OpenID Connect 1.0 UserService 时)。

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

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

ReactiveOAuth2UserService Configuration
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();

		return (userRequest) -> {
			// Delegate to the default implementation for loading a user
			return delegate.loadUser(userRequest)
					.flatMap((oidcUser) -> {
						OAuth2AccessToken accessToken = userRequest.getAccessToken();
						Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

						// TODO
						// 1) Fetch the authority information from the protected resource using accessToken
						// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

						// 3) Create a copy of oidcUser but use the mappedAuthorities instead
						oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

						return Mono.just(oidcUser);
					});
		};
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oidcUserService(): ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
        val delegate = OidcReactiveOAuth2UserService()

        return ReactiveOAuth2UserService { userRequest ->
            // Delegate to the default implementation for loading a user
            delegate.loadUser(userRequest)
                .flatMap { oidcUser ->
                    val accessToken = userRequest.accessToken
                    val mappedAuthorities = mutableSetOf<GrantedAuthority>()

                    // TODO
                    // 1) Fetch the authority information from the protected resource using accessToken
                    // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
                    // 3) Create a copy of oidcUser but use the mappedAuthorities instead
                    val mappedOidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo)

                    Mono.just(mappedOidcUser)
                }
        }
    }
}

OAuth 2.0 UserService

DefaultReactiveOAuth2UserService 是一个 ReactiveOAuth2UserService 的实现,支持标准的OAuth 2.0提供者。

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

DefaultReactiveOAuth2UserServiceUserInfo 端点请求用户属性时使用 WebClient

如果你需要定制 UserInfo 请求的预处理(pre-processing)和/或 UserInfo 响应的后处理(post-handling),你将需要为 DefaultReactiveOAuth2UserService.setWebClient() 提供一个自定义配置的 WebClient

无论你是定制 DefaultReactiveOAuth2UserService 还是提供你自己的 ReactiveOAuth2UserService 的实现,你都需要按照下面的例子来配置它。

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
		...
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oauth2UserService(): ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> {
        // ...
    }
}

OpenID Connect 1.0 UserService

OidcReactiveOAuth2UserService 是一个支持OpenID Connect 1.0提供者的 ReactiveOAuth2UserService 的实现。

OidcReactiveOAuth2UserService 在 UserInfo 端点请求用户属性时利用了 DefaultReactiveOAuth2UserService

如果你需要定制 UserInfo 请求的预处理(pre-processing)和/或UserInfo响应的后处理(post-handling),你将需要向 OidcReactiveOAuth2UserService.setOauth2UserService() 提供一个自定义配置的 ReactiveOAuth2UserService

无论你是定制 OidcReactiveOAuth2UserService 还是为OpenID Connect 1.0 Provider’s提供你自己的 ReactiveOAuth2UserService 实现,你都需要按照下面的例子来配置它。

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			...
			.oauth2Login(withDefaults());

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		...
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            oauth2Login { }
        }

        return http.build()
    }

    @Bean
    fun oidcUserService(): ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
        // ...
    }
}

ID Token 签名验证

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

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

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

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

下面的代码显示了如何配置 OidcIdTokenDecoderFactory @Bean,使其在所有 ClientRegistration 中默认为 MacAlgorithm.HS256

  • Java

  • Kotlin

@Bean
public ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
	ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
	idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
	return idTokenDecoderFactory;
}
@Bean
fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory<ClientRegistration> {
    val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory()
    idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }
    return idTokenDecoderFactory
}
对于基于 MAC 的算法,如 HS256HS384HS512,对应于 client-idclient-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 来实现,如下面的例子。

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

…​和 OidcClientInitiatedServerLogoutSuccessHandler,它实现了RP发起的注销,可以按以下方式进行配置。

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

	@Autowired
	private ReactiveClientRegistrationRepository clientRegistrationRepository;

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange(authorize -> authorize
				.anyExchange().authenticated()
			)
			.oauth2Login(withDefaults())
			.logout(logout -> logout
				.logoutSuccessHandler(oidcLogoutSuccessHandler())
			);

		return http.build();
	}

	private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
		OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
				new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository);

		// Sets the location that the End-User's User Agent will be redirected to
		// after the logout has been performed at the Provider
		oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");

		return oidcLogoutSuccessHandler;
	}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Autowired
    private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http {
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            oauth2Login { }
            logout {
                logoutSuccessHandler = oidcLogoutSuccessHandler()
            }
        }

        return http.build()
    }

    private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler {
        val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)

        // Sets the location that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}")
        return oidcLogoutSuccessHandler
    }
}
OidcClientInitiatedServerLogoutSuccessHandler 支持 {baseUrl} 占位符。如果使用,应用程序的 base URL,如 app.example.org ,将在请求时替换它。