Spring Security OAuth 之 @EnableAuthorizationServer 干了啥?
目前,几乎所有的 Spring 工程,都是以 Spring Boot 作为基础框架来搭建的,通过各种各样的 starter,我们可以在 Spring Boot 工程中方便地使用各种 Spring 和第三方的组件,比如,我们可以在引入 Spring Security OAuth 的 starter 后,可以方便地使用 @EnableAuthorizationServer
注解,在应用中自动开启和配置 Spring Security OAuth 的授权服务组件。
这篇文章,我们从这个注解入手,看看它是如何完成这些配置的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
从源码中可以看到,EnableAuthorizationServer
注解通过 @Import
,导入了两个配置文件:
AuthorizationServerEndpointsConfiguration
端点配置。AuthorizationServerSecurityConfiguration
安全配置。
接下来,一一分析。
AuthorizationServerEndpointsConfiguration 端点配置
在 Spring Security OAuth 的官方文档中,介绍了两个最重要的两个端点:
-
AuthorizationEndpoint
is used to service requests for authorization. Default URL:/oauth/authorize
.
-
TokenEndpoint
is used to service requests for access tokens. Default URL:/oauth/token
.
翻译一下:
- AuthorizationEndpoint 用来为授权请求提供服务,默认 URL 是:
/oauth/authorize
。 - TokenEndpoint 用来为访问令牌请求提供服务,默认 URL 是:
/oauth/token
。
这两个端点,都是在 AuthorizationServerEndpointsConfiguration 当中配置的,以下是这部分配置的源码:
@Bean
public AuthorizationEndpoint authorizationEndpoint() throws Exception {
AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(tokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
authorizationEndpoint.setRedirectResolver(redirectResolver());
return authorizationEndpoint;
}
@Bean
public TokenEndpoint tokenEndpoint() throws Exception {
TokenEndpoint tokenEndpoint = new TokenEndpoint();
tokenEndpoint.setClientDetailsService(clientDetailsService);
tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
tokenEndpoint.setTokenGranter(tokenGranter());
tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
return tokenEndpoint;
}
有了这两个端点的配置,客户端便可以向授权服务器请求授权和访问令牌,AuthorizationEndpoint 和 TokenEndpoint 两个端点的源码,里面包含了 OAuth 2.0 各个授权模式的处理逻辑,具体的源码分析,可以参考之前的文章,我把链接放在了文末。
AuthorizationServerSecurityConfiguration
在 AuthorizationServerSecurityConfiguration
中,我们看一下主要的配置代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// @formatter:off
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
// @formatter:on
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
从源码里主要可以看到 UserDetailsService 和 ClientDetailsService 的配置:
UserDetailsService
接口中唯一的抽象方法loadUserByUsername
用来通过用户名获取用户信息。ClientDetailsService
接口中唯一的抽象方法loadClientByClientId
用来通过客户端ID获取客户端信息。
这两个组件很类似,他们的作用也相同,只是一个针对用户(资源所有者),一个针对客户端。它们都会在认证授权的过程中使用到,用来对用户和客户端的信息进行校验。同样可以在文末的两篇文章中看到他们具体用在哪里。
在配置方法的开头,我们还能看到加载了 AuthorizationServerSecurityConfigurer
类的配置,我们在详细看一下。
AuthorizationServerSecurityConfigurer
这里的配置我们挑重点说。
ClientDetailsUserDetailsService
通过查看其源码,可以发现这里配置了一个 ClientDetailsUserDetailsService
。
@Override
public void init(HttpSecurity http) throws Exception {
registerDefaultAuthenticationEntryPoint(http);
if (passwordEncoder != null) {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());
clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());
http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(clientDetailsUserDetailsService)
.passwordEncoder(passwordEncoder());
}
else {
http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));
}
http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()
.httpBasic().realmName(realm);
if (sslOnly) {
http.requiresChannel().anyRequest().requiresSecure();
}
}
ClientDetailsUserDetailsService
这个类的名字起得容易让人摸不着头脑,我是这来解释一下:这是一个伪装成 U``serDetailsService
的 C``lientDetailsService
,它实现了 U``serDetailsService
并包含一个 C``lientDetailsService
类型的参数,它实现 loadUserByUsername
方法的方式就是在其中调用 C``lientDetailsService
的 loadClientByClientId
。
你可能会想,它是用来干什么用的?在配置 Spring Security OAuth 的时候,我们可以允许通过表单的方式提交 clientId
和 clientSecret
,此时,Spring Security 需要像认证用户信息一样,认证客户端信息,此时就会用到 ClientDetailsUserDetailsService
。
ClientCredentialsTokenEndpointFilter
此时,不得不说一下 AuthorizationServerSecurityConfigurer
中的另外一处配置。
@Override
public void configure(HttpSecurity http) throws Exception {
// ensure this is initialized
frameworkEndpointHandlerMapping();
if (allowFormAuthenticationForClients) {
clientCredentialsTokenEndpointFilter(http);
}
for (Filter filter : tokenEndpointAuthenticationFilters) {
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
clientCredentialsTokenEndpointFilter
.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setTypeName("Form");
authenticationEntryPoint.setRealmName(realm);
clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
return clientCredentialsTokenEndpointFilter;
}
这里通过 clientCredentialsTokenEndpointFilter
配置了一个 ClientCredentialsTokenEndpointFilter
过滤器。它的源代码,你可以自行查看一下,它和 UsernamePasswordAuthenticationFilter
非常相似,主要的不同点是:
- 它会从表单中读取 client_id 和 client_secret 来封装认证信息,而不是用户名和密码。
- 它查询用户信息的 UserDetailsService 实现类是
ClientDetailsUserDetailsService
。
也就是说,它通过验证用户名密码的方式,来验证客户端的信息。
最后
相关文章:
转载自:https://juejin.cn/post/7056668159100583944