Spring Security 的 OAuth 2.0 Client 特性为 OAuth 2.0 鉴权框架 中定义的
Client 角色提供了支持
高层的核心特性有:
在 OAuth 2.0 中,Authorization Grant 表示授权码类型
授权码是一个用来换取 Access Token 的 code,它代表着资源所有者对三方系统的授权
OAuth 2.0 支持多种类型的授权码,并且还支持扩展自定义的授权码类型
其中一种扩展,就是集成 Assertion 框架
将其中的 Assertion 作为授权码
而 Assertion 的具体实现又需要其它的框架支持,例如 SAML 和 JWT
另外,OAuth 2.0 还包括 Client 鉴权,即授权服务器认证三方系统身份的流程(可选的)
这个流程也支持 JWT 扩展
Servlet 环境的 WebClient
集成,就是一个 HTTP 客户端,用来发送 HTTP 请求
集成后,可以在 Client 后端发起访问资源服务的 HTTP 请求时使用
HttpSecurity.oauth2Client()
DSL 概览:
@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2ClientConfigurer -> oauth2ClientConfigurer
.clientRegistrationRepository(clientRegistrationRepository())
.authorizedClientRepository(authorizedClientRepository())
.authorizedClientService(authorizedClientService())
.authorizationCodeGrant(codeGrant -> codeGrant
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(authorizationRequestResolver())
.accessTokenResponseClient(accessTokenResponseClient())
)
);
}
}
ClientRegistration
代表一个注册到 OAuth 2.0 或 OpenID Connect 1.0 提供者(Provider)的 Client
一个 ClientRegistration
中保存了关于 Client 的信息:
package org.springframework.security.oauth2.client.registration;
public final class ClientRegistration {
private String registrationId; // ClientRegistration 的唯一标识
private String clientId; // OAuth2 标准中的 Client ID,向授权服务器标识 Client 的身份
private String clientSecret; // OAuth2 标准中的 Client Secret
/*
请求供应商(Provider)认证 Client 身份时使用的方法,支持的方法有:
client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt, none
*/
private ClientAuthenticationMethod clientAuthenticationMethod;
/*
OAuth 2.0 授权框架定义的授权码类型,支持的类型有:
authorization_code, client_credentials, password, as well as, urn:ietf:params:oauth:grant-type:jwt-bearer(扩展的类型)
*/
private AuthorizationGrantType authorizationGrantType;
/*
Client 注册到授权中心的重定向 URI
当终端用户鉴权成功,并为 Client 授权访问之后,授权中心会让用户代理(如:浏览器)重定向到这个 URI
*/
private String redirectUri;
private Set<String> scopes = Collections.emptySet(); // Client 请求的 scope(权限)如:openid, email, or profile
private ProviderDetails providerDetails = new ProviderDetails();
private String clientName; // Client 的一个描述性的名称,例如在登录页上展示三方登录的名称
public class ProviderDetails {
private String authorizationUri; // 授权服务器的授权接口 URI(Authorization Endpoint)
private String tokenUri; // 授权服务器用于通过授权码换取 Access Token 的接口(Token Endpoint)
private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint();
/*
从授权服务器查询 JSON Web Key (JWK) Set 的接口
JWK 中包含用于验证 ID Token(OpenID Connect 会用到) 的 JSON Web Signature (JWS) 的密钥
也可以用于验证 UserInfo 响应(从资源服务器获取的用户信息)
*/
private String jwkSetUri;
// 返回 OpenID Connect 1.0 提供者(Provider)或 OAuth 2.0 授权服务器的发行者标识
private String issuerUri;
/*
OpenID Provider Configuration Information
这个信息只有在 Spring Boot 2.x 配置了 spring.security.oauth2.client.provider.[providerId].issuerUri 属性时可用
*/
private Map<String, Object> configurationMetadata = Collections.emptyMap();
public class UserInfoEndpoint {
// 向资源服务器查询终端用户信息的 URI(UserInfo Endpoint)
private String uri;
// 用 Access Token 访问 UserInfo Endpoint 获取用户信息时使用的的方法
private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
// 访问 UserInfo Endpoint 后返回的用户信息响应(UserInfo Response)中,用户名的参数名称(用户名泛指唯一标识终端用户的信息)
private String userNameAttributeName;
}
}
}
参考:JSON Web Key (JWK) 、JSON Web Signature (JWS) 、OpenID Provider Configuration Information
一个 ClientRegistration
可以通过下边两种发现机制进行配置
ClientRegistrations
提供了通过这种发现机制来配置 ClientRegistration
的方法:
@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {
ClientRegistration clientRegistration() {
return ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
}
}
上边的代码会依次访问下边的 URL 直到获得 200 状态码:
使用第一个 200 状态码的响应来配置 ClientRegistration
也可以使用 ClientRegistrations.fromOidcIssuerLocation(String issuer)
这个方法只会访问 OpenID Connect 提供者(Provider)的 Configuration endpoint
ClientRegistrationRepository
作为存储 OAuth 2.0 / OpenID Connect 1.0 的 ClientRegistration
的仓库
Client 注册信息由相关的授权服务器所有,最终也是以存储在授权服务器的信息为准
ClientRegistrationRepository
存储的是完整 Client 注册信息的一个子集,并且需要与授权服务器存储的注册信息保持一致
ClientRegistrationRepository
是否通过授权服务器提供的接口同步 Client 注册信息,以及如何同步取决于具体实现
ClientRegistrationRepository
的默认实现是 InMemoryClientRegistrationRepository
用一个 Map<String, ClientRegistration>
存储 registrationId -> ClientRegistration
映射
Spring Boot 2.x 的自动配置会将 spring.security.oauth2.client.registration.[registrationId] 下的属性绑定到 ClientRegistration
实例
然后将这些 ClientRegistration
存入 ClientRegistrationRepository
自动配置还会将 ClientRegistrationRepository
注册为 ApplicationContext
中的 @Bean
,在应用中,可以通过依赖注入获取
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@GetMapping("foo/registration")
public String fooRegistration() {
ClientRegistration fooRegistration = clientRegistrationRepository.findByRegistrationId("foo");
return fooRegistration == null ? "fooRegistration == null" : fooRegistration.toString();
}
}
OAuth2AuthorizedClient
代表一个已被授权的 Client
当终端用户(资源拥有者)向某个 Client 授予了访问受保护资源的权限时,这个 Client 被认为是已被授权的 Client
OAuth2AuthorizedClient
将 OAuth2AccessToken
(可选的 OAuth2RefreshToken
)和 ClientRegistration
与资源拥有者关联起来
其中的 Principal
指的是终端用户(资源拥有者)
OAuth2AuthorizedClientRepository
负责在 Web 请求之间持久化 OAuth2AuthorizedClient
(通常对应会话域)
从开发者的角度来看,OAuth2AuthorizedClientRepository
或者 OAuth2AuthorizedClientService
提供了查找某个 Client 对应的 OAuth2AccessToken
的方法
在向资源服务器发起访问用户资源的请求时,必须要先获取 OAuth2AccessToken
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private OAuth2AuthorizedClientService oauth2AuthorizedClientService;
// ...
@GetMapping("foo/accesstoken")
public String fooAccessToken(Authentication authentication) {
OAuth2AuthorizedClient authorizedClient = oauth2AuthorizedClientService
.loadAuthorizedClient("foo", authentication.getName());
return authorizedClient.getAccessToken().getTokenType().getValue();
}
}
Spring Boot 2.x 的自动配置在 ApplicationContext
注册了 OAuth2AuthorizedClientRepository
和/或 OAuth2AuthorizedClientService
@Bean
客户端程序员也可以注册自己的 OAuth2AuthorizedClientRepository
或 OAuth2AuthorizedClientService
@Bean
OAuth2AuthorizedClientService
的默认实现是 InMemoryOAuth2AuthorizedClientService
在内存中存储 OAuth2AuthorizedClient
可以配置 JdbcOAuth2AuthorizedClientService
通过 JDBC 将 OAuth2AuthorizedClient
持久化到数据库
JdbcOAuth2AuthorizedClientService
依赖 OAuth 2.0 Client Schema 中的数据库表结构
OAuth2AuthorizedClientManager
负责对 OAuth2AuthorizedClient
进行管理
其主要责任有:
OAuth2AuthorizedClientProvider
为 OAuth 2.0 Client 授权(或重新授权)OAuth2AuthorizedClientService
或 OAuth2AuthorizedClientRepository
对 OAuth2AuthorizedClient
进行持久化OAuth2AuthorizationSuccessHandler
进行处理OAuth2AuthorizationFailureHandler
进行处理OAuth2AuthorizedClientProvider
会实现一个为 OAuth 2.0 Client 授权(或重新授权)的策略
一般会实现一种授权码类型(Authorization Grant),例如:authorization_code、client_credentials 等
OAuth2AuthorizedClientManager
的默认实现是 DefaultOAuth2AuthorizedClientManager
这个实现关联着一个基于委派组合的支持多种授权码类型的 OAuth2AuthorizedClientProvider
这个实现是用 OAuth2AuthorizedClientProviderBuilder
构建的 DelegatingOAuth2AuthorizedClientProvider
对象
客户端程序员也可以使用 OAuth2AuthorizedClientProviderBuilder
配置并构建一个基于委派组合的 DelegatingOAuth2AuthorizedClientProvider
下边的例子展示了如何用 OAuth2AuthorizedClientProviderBuilder
配置并构建一个 DelegatingOAuth2AuthorizedClientProvider
支持:authorization_code、refresh_token、client_credentials、基于密码的授权
@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
授权成功后,DefaultOAuth2AuthorizedClientManager
会调用 OAuth2AuthorizationSuccessHandler.onAuthorizationSuccess()
OAuth2AuthorizationSuccessHandler
默认会通过 OAuth2AuthorizedClientRepository
保存 OAuth2AuthorizedClient
重新授权失败后,例如 Refresh Token 失效了
RemoveAuthorizedClientOAuth2AuthorizationFailureHandler
会将之前保存的 OAuth2AuthorizedClient
从 OAuth2AuthorizedClientRepository
中移除
可以通过 setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)
和 setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)
配置自定义处理器
DefaultOAuth2AuthorizedClientManager
持有一个 contextAttributesMapper,类型为 Function<OAuth2AuthorizeRequest, Map<String, Object>>
负责将 OAuth2AuthorizeRequest
中的属性放入一个属性值的 Map
,这个 Map
的值会被加入到 OAuth2AuthorizationContext
中
某些 OAuth2AuthorizedClientProvider
需要用到请求中的参数,这时就可以通过这种方式获取到
例如 PasswordOAuth2AuthorizedClientProvider
需要从 OAuth2AuthorizationContext.getAttributes()
中获取资源拥有者的用户名和密码
contextAttributesMapper 这个函数式提供了一个接入点,可以自定义从请求获取需要属性的具体方式:
@Component
public class Oauth2Config extends WebSecurityConfigurerAdapter {
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
/*
假设 `用户名` 和 `密码` 由 `HttpServletRequest` 参数提供
将 `HttpServletRequest` 参数映射到 `OAuth2AuthorizationContext.getAttributes()`
*/
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return oauth2AuthorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = oauth2AuthorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = null, password = null;
if (servletRequest != null) {
username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
}
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` 需要这两个属性
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
}
DefaultOAuth2AuthorizedClientManager
被设计为在 HttpServletRequest
上下文中使用
如果操作不在 HttpServletRequest
上下文中,则应该使用 AuthorizedClientServiceOAuth2AuthorizedClientManager
服务应用(Service Application)是一个常见的使用 AuthorizedClientServiceOAuth2AuthorizedClientManager
的场景
服务应用通常在后端运行,不与用户交互,通常使用系统级别的账户而不是普通用户账户
此时可以使用 client_credentials 类型的授权码
下边是一个通过配置 AuthorizedClientServiceOAuth2AuthorizedClientManager
支持 client_credentials 类型授权码的例子:
@Component
public class A04_ClientCredentialOauth2ClientConfig extends WebSecurityConfigurerAdapter {
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager
authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
WebClient