likes
comments
collection
share

Spring Security OAuth 之 @EnableAuthorizationServer 干了啥?

作者站长头像
站长
· 阅读数 43

目前,几乎所有的 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``serDetailsServiceC``lientDetailsService,它实现了 U``serDetailsService 并包含一个 C``lientDetailsService 类型的参数,它实现 loadUserByUsername 方法的方式就是在其中调用 C``lientDetailsServiceloadClientByClientId

你可能会想,它是用来干什么用的?在配置 Spring Security OAuth 的时候,我们可以允许通过表单的方式提交 clientIdclientSecret,此时,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 非常相似,主要的不同点是:

  1. 它会从表单中读取 client_id 和 client_secret 来封装认证信息,而不是用户名和密码。
  2. 它查询用户信息的 UserDetailsService 实现类是 ClientDetailsUserDetailsService

也就是说,它通过验证用户名密码的方式,来验证客户端的信息。

最后

相关文章:

转载自:https://juejin.cn/post/7056668159100583944
评论
请登录