31. OAuth 2.0 登录 — 高级配置

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

例如,oauth2Login().authorizationEndpoint() 允许配置 Authorization Endpoint,而 oauth2Login().tokenEndpoint() 允许配置 Token Endpoint。

下面的代码展示了一个例子:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.authorizationEndpoint()
					...
				.redirectionEndpoint()
					...
				.tokenEndpoint()
					...
				.userInfoEndpoint()
					...
	}
}

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

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

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

  • Authorization Endpoint:由客户端使用以通过用户代理重定向从资源所有者获得授权。
  • Token Endpoint:客户端用来交换访问令牌的授权授权,通常与客户端身份验证相交换。

以及一个客户端端点:

  • Redirection Endpoint:授权服务器用来通过资源所有者用户代理返回包含授权凭证的响应给客户端。

OpenID Connect Core 1.0 规范定义了用户信息端点如下:

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

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

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.clientRegistrationRepository(this.clientRegistrationRepository())
				.authorizedClientService(this.authorizedClientService())
				.loginPage("/login")
				.authorizationEndpoint()
					.baseUri(this.authorizationRequestBaseUri())
					.authorizationRequestRepository(this.authorizationRequestRepository())
					.and()
				.redirectionEndpoint()
					.baseUri(this.authorizationResponseBaseUri())
					.and()
				.tokenEndpoint()
					.accessTokenResponseClient(this.accessTokenResponseClient())
					.and()
				.userInfoEndpoint()
					.userAuthoritiesMapper(this.userAuthoritiesMapper())
					.userService(this.oauth2UserService())
					.oidcUserService(this.oidcUserService())
					.customUserType(GitHubOAuth2User.class, "github");
	}
}

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

  • 小节 31.1, “OAuth 2.0 登录页”
  • 小节 31.2, “Authorization Endpoint”
  • 小节 31.3, “Redirection Endpoint”
  • 小节 31.4, “Token Endpoint”
  • 小节 31.5, “UserInfo Endpoint”

31.1 OAuth 2.0 登录页

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

每个 OAuth Client 的链接的目的地默认为以下内容:

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"

下面的行展示了一个例子:

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

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

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

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.loginPage("/login/oauth2")
				...
				.authorizationEndpoint()
					.baseUri("/login/oauth2/authorization")
					....
	}
}
[Important] 重要的

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

[Tip] Tip

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

下面的行展示了一个例子:

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

31.2 Authorization Endpoint

31.2.1 AuthorizationRequestRepository

AuthorizationRequestRepository 负责 OAuth2AuthorizationRequest 从启动 Authorization Request 到接收 Authorization Response(回调)的持续性。

[Tip] Tip

使用 OAuth2AuthorizationRequest 来关联和验证 Authorization Response。

AuthorizationRequestRepository 的默认实现是 HttpSessionOAuth2AuthorizationRequestRepository,它在 HttpSession 中存储 OAuth2AuthorizationRequest。

如果你想提供在 Cookie 中存储 OAuth2AuthorizationRequest 属性的 AuthorizationRequestRepository 的自定义实现,请配置它,如下面的示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.authorizationEndpoint()
					.authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
					...
	}

	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
		return new HttpCookieOAuth2AuthorizationRequestRepository();
	}
}

31.3 Redirection Endpoint

授权服务器使用 Redirection Endpoint 通过 Resource Owner 用户代理将授权响应(包含授权凭证)返回给客户端。

[Tip] Tip

OAuth 2.0 登录使用授权代码授予。因此,授权凭证是授权码。

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

如果你想自定义授权响应 baseUri,请配置它,如下面的示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.redirectionEndpoint()
					.baseUri("/login/oauth2/callback/*")
					....
	}
}
[Important] 重要的

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

下面的列展示了一个示例:

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

31.4 Token Endpoint

31.4.1 OAuth2AccessTokenResponseClient

OAuth2AccessTokenResponseClient 负责在授权服务器的 Token Endpoint 为访问令牌凭证交换授权授权凭证。

OAuth2AccessTokenResponseClient 的默认实现是 NimbusAuthorizationCodeTokenResponseClient,它在 Token Endpoint 交换访问令牌的授权代码。

[Note] Note

NimbusAuthorizationCodeTokenResponseClient 在内部使用 Nimbus OAuth 2.0 SDK。

如果你想提供使用 Spring Framework 5 的响应式 WebClient 启动 Token Endpoint 请求的 OAuth2AccessTokenResponseClient,请配置它,如下面的示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.tokenEndpoint()
					.accessTokenResponseClient(this.accessTokenResponseClient())
					...
	}

	private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
		return new SpringWebClientAuthorizationCodeTokenResponseClient();
	}
}

31.5 UserInfo Endpoint

UserInfo Endpoint 包含若干配置选项,如以下子部分所述:

  • 小节 31.5.1, “映射用户授权”
  • 小节 31.5.2, “配置一个自定义 OAuth2User”
  • 小节 31.5.3, “OAuth 2.0 UserService”
  • 小节 31.5.4, “OpenID Connect 1.0 UserService”

31.5.1 映射用户授权

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

[Tip] Tip

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

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

  • 使用 GrantedAuthoritiesMapper
  • OAuth2UserService 的基于委托策略

使用 GrantedAuthoritiesMapper

提供 GrantedAuthoritiesMapper 的实现并配置它,如下面的示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.userAuthoritiesMapper(this.userAuthoritiesMapper())
					...
	}

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

或者,可以注册 GrantedAuthoritiesMapper @Bean 以将其自动应用于配置,如下面的示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.oauth2Login();
	}

	@Bean
	public GrantedAuthoritiesMapper userAuthoritiesMapper() {
		...
	}
}

OAuth2UserService 的基于委托策略

但是,与使用 GrantedAuthoritiesMapper 相比,这个策略是先进的,因为它允许你访问 OAuth2UserRequest 和 OAuth2User(当使用 OAuth 2.0 UserService时)或 OidcUserRequest 和 OidcUser(当使用 OpenID Connect 1.0 UserService 时)。

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

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

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.oidcUserService(this.oidcUserService())
					...
	}

	private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		final OidcUserService delegate = new OidcUserService();

		return (userRequest) -> {
			// Delegate to the default implementation for loading a user
			OidcUser oidcUser = delegate.loadUser(userRequest);

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

31.5.2 配置一个自定义 OAuth2User

CustomUserTypesOAuth2UserService 是 OAuth2UserService 的一种实现,它为自定义 OAuth2User 类型提供支持。

如果默认实现(DefaultOAuth2User)不适合你的需求,你可以定义自己的 OAuth2User 的实现。

下面的代码演示了如何为 GitHub 注册自定义 OAuth2User 类型:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.customUserType(GitHubOAuth2User.class, "github")
					...
	}
}

下面的代码展示了为 GitHub 自定义 OAuth2User 类型的示例:

public class GitHubOAuth2User implements OAuth2User {
	private List<GrantedAuthority> authorities =
		AuthorityUtils.createAuthorityList("ROLE_USER");
	private Map<String, Object> attributes;
	private String id;
	private String name;
	private String login;
	private String email;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.authorities;
	}

	@Override
	public Map<String, Object> getAttributes() {
		if (this.attributes == null) {
			this.attributes = new HashMap<>();
			this.attributes.put("id", this.getId());
			this.attributes.put("name", this.getName());
			this.attributes.put("login", this.getLogin());
			this.attributes.put("email", this.getEmail());
		}
		return attributes;
	}

	public String getId() {
		return this.id;
	}

	public void setId(String id) {
		this.id = id;
	}

	@Override
	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getLogin() {
		return this.login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public String getEmail() {
		return this.email;
	}

	public void setEmail(String email) {
		this.email = email;
	}
}
[Tip] Tip

id、name、login 和 email 是在 GitHub 的 UserInfo Response 中返回的属性。有关从 UserInfo Endpoint 返回的详细信息,请参阅 "获取认证用户" 的 API 文档。

31.5.3 OAuth 2.0 UserService

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

[Note] Note

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

如果默认实现不适合你的需要,你可以为标准的 OAuth 2.0 提供程序定义自己的 OAuth2UserService 实现。

下面的配置演示如何配置自定义的 OAuth2UserService:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.userService(this.oauth2UserService())
					...
	}

	private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
		return new CustomOAuth2UserService();
	}
}

31.5.4 OpenID Connect 1.0 UserService

OidcUserService 是支持 OpenID Connect 1.0 Provider 的 OAuth2UserService 的实现。

[Note] Note

OAuth2UserService 负责从 UserInfo Endpoint(通过使用在授权流期间授予客户端的访问令牌)获取最终用户(资源所有者)的用户属性,并以 OidcUser 的形式返回 AuthenticatedPrincipal。

如果默认实现不适合你的需要,那么你可以定义自己的 OpenID Connect 1.0 Provider 的 OAuth2UserService 实现。

下面的配置演示如何配置自定义 OpenID Connect 1.0 OAuth2UserService:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.oidcUserService(this.oidcUserService())
					...
	}

	private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		return new CustomOidcUserService();
	}
}