# 高级配置

OAuth2.0 授权框架将协议端点 (opens new window)定义如下:

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

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

  • 令牌端点:由客户端使用,用于将授权许可交换为访问令牌,通常与客户端身份验证一起使用。

以及一个客户端端点:

  • 重定向端点:授权服务器用于通过资源所有者 User-Agent 向客户端返回包含授权凭据的响应。

OpenID Connect Core1.0 规范将用户信息端点 (opens new window)定义如下:

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

ServerHttpSecurity.oauth2Login()提供了许多用于定制 OAuth2.0 登录的配置选项。

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

例 1。OAuth2 登录配置选项

爪哇

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

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            oauth2Login {
                authenticationConverter = authenticationConverter()
                authenticationMatcher = authenticationMatcher()
                authenticationManager = authenticationManager()
                authenticationSuccessHandler = authenticationSuccessHandler()
                authenticationFailureHandler = authenticationFailureHandler()
                clientRegistrationRepository = clientRegistrationRepository()
                authorizedClientRepository = authorizedClientRepository()
                authorizedClientService = authorizedClientService()
                authorizationRequestResolver = authorizationRequestResolver()
                authorizationRequestRepository = authorizationRequestRepository()
                securityContextRepository = securityContextRepository()
            }
        }
    }
}

以下小节将详细介绍每种可用的配置选项:

# OAuth2.0 登录页面

默认情况下,OAuth2.0 登录页面是由LoginPageGeneratingWebFilter自动生成的。默认的登录页面显示了每个配置的 OAuth 客户机,其ClientRegistration.clientName作为链接,该链接能够发起授权请求(或 OAuth2.0 登录)。

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

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

"/oauth2/authorization/{registrationId}"

下面的一行显示了一个示例:

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

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

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

例 2。OAuth2 登录页面配置

爪哇

@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);
	}

	...
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

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

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

        return DefaultServerOAuth2AuthorizationRequestResolver(
            clientRegistrationRepository(), authorizationRequestMatcher
        )
    }

    ...
}
你需要提供一个@Controller和一个@RequestMapping("/login/oauth2"),它能够呈现自定义登录页面。
如前所述,配置oauth2Login().authorizationRequestResolver()是可选的。
但是,如果你选择自定义它,请确保到每个 OAuth 客户机的链接匹配通过ServerWebExchangeMatcher提供的模式。

下面的一行显示了一个示例:

# 重定向端点

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

OAuth2.0Login 利用授权代码授权。
因此,授权凭据是授权代码。

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

如果你希望自定义授权响应重定向端点,请按照下面的示例配置它:

例 3。重定向端点配置

爪哇

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

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

		return http.build();
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            oauth2Login {
                authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")
            }
        }
    }
}
你还需要确保ClientRegistration.redirectUri与自定义授权响应重定向端点相匹配。

下面的列表显示了一个示例:

爪哇
<br/>return CommonOAuth2Provider.GOOGLE.getBuilder("google")<br/> .clientId("google-client-id")<br/> .clientSecret("google-client-secret")<br/> .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")<br/> .build();<br/>
Kotlin <>r=”64“/>><gt="r="R="53"/>

# 用户信息端点

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

# 映射用户权限

在用户成功地使用 OAuth2.0 提供程序进行身份验证之后,OAuth2User.getAuthorities()(或OidcUser.getAuthorities())可以被映射到一组新的GrantedAuthority实例,在完成身份验证时,该实例将被提供给OAuth2AuthenticationToken

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

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

# 使用 grantedauthoritiesmapper

注册一个GrantedAuthoritiesMapper``@Bean以使其自动应用于配置,如以下示例所示:

例 4。授予权限映射器配置

爪哇

@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;
		};
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

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

    @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(当使用 OAuth2.0 用户服务时)或OidcUserRequestOidcUser(当使用 OpenID Connect1.0 用户服务时)。

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

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

例 5。ReactiveOAuth2 用户服务配置

爪哇

@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);
					});
		};
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

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

    @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)
                }
        }
    }
}

# OAuth2.0 用户服务

DefaultReactiveOAuth2UserService是支持标准 OAuth2.0 提供者的ReactiveOAuth2UserService的实现。

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

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

如果需要自定义用户信息请求的预处理和/或用户信息响应的后处理,则需要为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() {
		...
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

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

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

# OpenID Connect1.0 用户服务

OidcReactiveOAuth2UserService是支持 OpenID Connect1.0Provider 的ReactiveOAuth2UserService的实现。

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

如果需要自定义用户信息请求的预处理和/或用户信息响应的后处理,则需要为OidcReactiveOAuth2UserService.setOauth2UserService()提供自定义配置的ReactiveOAuth2UserService

无论你是定制OidcReactiveOAuth2UserService还是为 OpenID Connect1.0Provider 提供你自己的ReactiveOAuth2UserService实现,你都需要按照下面的示例配置它:

爪哇

@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {

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

		return http.build();
	}

	@Bean
	public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		...
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

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

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

# ID 令牌签名验证

OpenID Connect1.0 身份验证引入了ID Token (opens new window),这是一种安全令牌,其中包含关于客户端使用授权服务器对最终用户进行身份验证的声明。

ID 令牌表示为JSON 网络令牌 (opens new window),并且必须使用JSON 网页签名 (opens new window)进行签名。

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

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

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

爪哇

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

Kotlin

@Bean
fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory<ClientRegistration> {
    val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory()
    idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }
    return idTokenDecoderFactory
}
对于基于 MAC 的算法,如HS256HS384HS512,对应于client-secretclient-secret被用作签名验证的对称密钥。
如果一个以上的ClientRegistration被配置为 OpenID Connect1.0 身份验证,则 JWS 算法解析器可以对提供的ClientRegistration进行评估,以确定返回哪个算法。

# OpenID Connect1.0 注销

OpenID Connect会话Management1.0 允许使用客户端在提供商处注销最终用户。可用的策略之一是RP 启动的注销 (opens new window)

如果 OpenID 提供程序同时支持会话管理和Discovery (opens new window),则客户端可以从 OpenID 提供程序的end_session_endpoint``URL中获得发现元数据 (opens new window)。这可以通过配置ClientRegistrationissuer-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-initedlogout,可以配置如下:

Java

@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;
	}
}

Kotlin

@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {

    @Autowired
    private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository

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

    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}占位符。
如果使用的话,应用程序的基本 URL,如[https://app.example.org](https://app.example.org),将在请求时替换它。

核心配置OAuth2 客户端