Security OAuth2 SSO单点登录源码剖析(二)

Security OAuth2 SSO单点登录源码剖析 - 自己总结 - 有道笔记~

OAuth2授权流程和源码解析 - 自己总结 - 有道笔记~

第三方登录源码梳理 - 自己总结 - 有道笔记~

Security OAuth2 SSO单点登录(一)

Security OAuth2 授权 & JWT

Spring Security框架配置运行流程完整分析 - 自己总结

多维系统下单点登录深入详解

单点登录简介

SSO客户端

@EnableOAuth2Sso注解

@EnableOAuth2Sso是sso客户端的核心注解,它为sso客户端引入了与单点登录相关的配置。
由该注解引入的配置包括:
@EnableOAuth2Client、
OAuth2SsoProperties、
OAuth2SsoDefaultConfiguration、
OAuth2SsoCustomConfiguration、
ResourceServerTokenServicesConfiguration

@EnableOAuth2Client注解

@EnableOAuth2Client使用@Import注解引入了OAuth2ClientConfiguration配置类

OAuth2ClientConfiguration

1.在OAuth2ClientConfiguration配置类中,使用@Bean方式定义了OAuth2ClientContextFilter过滤器,嵌入到Web容器中(注意,是Web容器,并不是security的过滤器链中),当后续的过滤器抛出UserRedirectRequiredException这种类型的异常时,就会让用户重定向到认证中心去做登录。
那么,就要保证OAuth2ClientContextFilter过滤器一定是要排在FilterChainProxy的前面的,从【OAuth2授权流程和源码解析】的【FilterChainProxy与其它的web的Filter的顺序】章节,可以知道默认的springSecurityFilterChain的排序值为-100,如果要排在它的前面,那么就需要小于-100,我们可以看到在OAuth2AutoConfiguration自动配置类中引入了OAuth2RestOperationsConfiguration配置类,这个OAuth2RestOperationsConfiguration配置类中就注入了上面使用@Bean方式定义的OAuth2ClientContextFilter过滤器,并且使用FilterRegistrationBean将它包装并交给spring容器,注意到它设置的order恰好就是:-100-10 = -100,比-100要小,因此,OAuth2ClientContextFilter排在springSecurityFilterChain的前面是没有问题的。

OAuth2ClientConfiguration中定义OAuth2ClientContextFilter如下:

@Bean
public OAuth2ClientContextFilter oauth2ClientContextFilter() {
    // 当后续的过滤器抛出UserRedirectRequiredException这种类型的异常时,该OAuth2ClientContextFilter过滤器就会让用户重定向到认证中心去做登录
    OAuth2ClientContextFilter filter = new OAuth2ClientContextFilter();
    return filter;
}

OAuth2RestOperationsConfiguration中定义FilterRegistrationBean<OAuth2ClientContextFilter>如下:

@Configuration
@ConditionalOnBean(OAuth2ClientConfiguration.class)            // 当引入了OAuth2ClientConfiguration时, 当前配置类才会生效
@Conditional({ OAuth2ClientIdCondition.class,                 // 是否有配置: security.oauth2.client.client-id 对应的值
               NoClientCredentialsCondition.class })           // security.oauth2.client.grant_type值不为client_credentials, 如果该值未明确指定该配置的值为client_credentials, 那么这个条件会满足
               
@Import(OAuth2ProtectedResourceDetailsConfiguration.class)     // 引入了OAuth2ProtectedResourceDetailsConfiguration配置类, 
                                                               // 该配置类中定义了AuthorizationCodeResourceDetails这个bean并作为primary, 默认grantType写死为: authorization_code,
                                                               // 并且会读取security.oauth2.client下的配置属性, 这些属性都定义在BaseOAuth2ProtectedResourceDetails中, 其中包括:
                                                               // id、grantType、clientId、accessTokenUri、scope、clientSecret、clientAuthenticationScheme、authorizationScheme、tokenName                                                                                                                                               
protected static class SessionScopedConfiguration {

    // 自动注入OAuth2ClientConfiguration中定义的OAuth2ClientContextFilter, 
    // 自动注入以spring.security为配置前缀的SecurityProperties, 可配置项有: filter.order、filter.dispatcherTypes、user.name、user.password、user.roles、user.passwordGenerated
    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter, SecurityProperties security) {
      FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<>();
      registration.setFilter(filter);
      
      // 设定OAuth2ClientContextFilter的顺序, 默认为: -110
      registration.setOrder(security.getFilter().getOrder() - 10);
      return registration;
    }
    
}

2.OAuth2ClientConfiguration中使用了@Scope注解,分别定义了AccessTokenRequest和OAuth2ClientContext
@Scope(value = “request”, proxyMode = ScopedProxyMode.INTERFACES)的方式定义了AccessTokenRequest、
@Scope(value = “session”, proxyMode = ScopedProxyMode.INTERFACES)的方式定义了OAuth2ClientContext
(关于@Scope注解的原理和使用示例可以参考:https://blog.csdn.net/qq_16992475/article/details/122562271、https://www.processon.com/view/link/62d5681ce401fd5b52bfde5f)
代码如下:

/* 定义了AccessTokenRequest 这个bean, 它的scope为request */
/* 1. 由这种方式定义的bean, 将会使用ScopedProxyFactoryBean创建1个代理对象作为bean, 该代理对象bean的名beanName为accessTokenRequest,
      当调用这个代理对象bean的任何方法时, 会触发beanFactory的getBean("ScopedTarget.accessTokenRequest")方法, 由于该bean定义的scope为request,
      从而由RequestScope中的逻辑获取请求域内的bean, RequestScope获取bean的逻辑就是: 在1次请求中, 多次使用beanFactory去getBean时, 拿到的其实是同一个对象。
   2. 当某一次请求去调用这个代理对象的方法时, 由于第一次调用, 请求域内没有这个bean, 并且由于这个bean是以@Bean的方式定义在容器中, 
      所以@Bean所修饰的方法参数将作为依赖解析出来, 然后将@Bean方法所返回的对象作为bean,存入到请求域内, 
      其实就是放到RequestContextHolder.currentRequestAttributes()中, 并以request作为key, 
      2.1 当仍在当前请求时, 继续调用这个代理对象的方法时, 由于请求域内已经有了, 因此就不会再去触发@Bean方法执行了。
      2.2 当在下一次请求时, 再调用这个代理对象的方法时, 就会再次触发@Bean方法的执行。
   3. 使用这种方式定义的好处就是, 外界只需要注入这个accessTokenRequest的bean, 每次等到要使用这个bean时, 也就是调用这个bean的方法时, 
      它才会去触发getBean, 有点像@Lazy, 但是比@Lazy好, 因为使用@Lazy时, 外界是需要加@Lazy这个注解的, 是有感知的, 而使用这个外界的代码可以不加@Lazy, 外界没感知
  */
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
                                                // 这里的spel表达式, 能写request的原因是调用RequestScope的resolveContextualObject(key), 
                                                //                  其中key就是request, 因此能获取到当前请求对象
protected AccessTokenRequest accessTokenRequest(@Value("#{request.parameterMap}") Map<String, String[]> parameters,  
                                                @Value("#{request.getAttribute('currentUri')}") String currentUri) {   
   // 默认的实现用的是: DefaultAccessTokenRequest
   DefaultAccessTokenRequest request = new DefaultAccessTokenRequest(parameters);   
   // 将请求中的currrentUri属性设置到DefaultAccessTokenRequest的currentUri属性中,
   // 在OAuth2ClientContextFilter过滤器中, 都会记录当前请求的uri到request的currentUri属性中,
   request.setCurrentUri(currentUri);   
   return request;
}

@Configuration
protected static class OAuth2ClientContextConfiguration {
   
   /* 注入 AccessTokenRequest, 就上面定义的AccessTokenRequest, 如上所说, 就是个代理对象而已 */
   @Resource
   @Qualifier("accessTokenRequest")
   private AccessTokenRequest accessTokenRequest;
   
   /* 定义了oauth2ClientContext 这个bean, 它的scope为session */
   /* 同如上定义accessTokenRequest的bean, 只不过这里定义的是SessionScope, 
      即外界只需要注入 OAuth2ClientContext这个bean, 在一个会话内, 使用的都是同1个bean
   */
   @Bean
   @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
   public OAuth2ClientContext oauth2ClientContext() {
      return new DefaultOAuth2ClientContext(accessTokenRequest);
   }
   
}

OAuth2SsoProperties

配置前缀为:security.oauth2.sso,可配置就只是:security.oauth2.sso.login-path = /login
当用户未登录时,访问sso客户端资源,如:http://localhost:1111,抛出认证异常或访问拒绝异常而被异常处理器捕捉到该异常,就会将用户重定向到sso客户端的该路径下,如:http://localhost:1111/login,但此时会让OAuth2ClientAuthenticationProcessingFilter认证过滤器满足此路径匹配要求,因为现在没有任何令牌相关的信息,所以就会抛出UserRedirectRequiredException。而这种类型的异常一旦抛出,就会让OAuth2ClientContextFilter这个过滤器捕捉到,这个OAuth2ClientContextFilter过滤器就会让用户去重定向到认证服务器授权页面去授权:http://localhost:1110/oauth/authorize?client_id=c1&redirect_uri=http://localhost:1111/login&response_type=code&state=Z2Io35。
但由于当前用户在认证中心未登录,就会又被认证服务器重定向到认证登录页面,当用户登录完成时,按照OAuth2授权码一般的授权逻辑,就会重定向到授权页面,但这里会由于走自动授权模式,因此就直接重定向到了:http://localhost:1111/login?code=cSdvsL&state=M28wuM,此时就携带了code和state,而一旦拿到了code和state,在OAuth2ClientAuthenticationProcessingFilter过滤器中,就可以按照授权码模式的逻辑获取访问令牌,获取用户信息了

OAuth2SsoDefaultConfiguration

这个配置类继承自WebSecurityConfigurerAdapter类;
这个配置类只会在当使用了@EnableOAuth2Sso注解时,但是这个@EnableOAuth2Sso注解没有标注在WebSecurityConfigurerAdapter的子类上时才会生效;
此时,它会保护所有的路径,并且创建SsoSecurityConfigurer配置器来配置HttpSecurity,即这个SsoSecurityConfigurer配置器会往过滤器链中添加sso相关的过滤器:OAuth2ClientAuthenticationProcessingFilter,然后往ExceptionTranslationFilter中的entryPoint添加LoginUrlAuthenticationEntryPoint重定向入口相关设置。

OAuth2SsoCustomConfiguration

这个类实现了ImportAware、BeanPostProcessor、ApplicationContextAware这3个接口;
这个配置类只会在当使用了@EnableOAuth2Sso注解时,并且这个@EnableOAuth2Sso注解标注在WebSecurityConfigurerAdapter的子类上时才会生效;
这是个比较重要的类,详细的分析下代码的用意:
1.OAuth2SsoCustomConfiguration实现ImportAware接口的意义在于,在重写的setImportMetadata方法中,能够拿到@EnableOAuth2Sso注解所标注的类,拿到标注的类后,就可以为它创建动态代理,如何创建它的动态代理,接着往下看。

@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
    // 拿到@EnableOAuth2Sso注解所标注的类
    this.configType = ClassUtils.resolveClassName(importMetadata.getClassName(),null);
}

2.OAuth2SsoCustomConfiguration实现BeanPostProcessor接口的意义在于,在创建完@EnableOAuth2Sso注解所标注的类作为bean后,在BeanPostProcessor后置处理器的postProcessAfterInitialization方法中,即此时的bean已完成属性赋值了,使用动态的方式将这个bean给替换掉,替换的方式是使用了ProxyFactory,这个可以学习下如何快速的创建目标对象的代理对象,代理的增强逻辑在SsoSecurityAdapter中,使用代理对象替换掉@EnableOAuth2Sso注解所标注的类作为bean的目的是为了修改WebSecurityConfigurerAdapter的init(WebSecurity)方法,那么是如何修改这个方法的,又改变了什么,接着往下看

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    
   // 如果bean所属的class就是@EnableOAuth2Sso注解所标注的类, 
   // 那么就调用ProxyFactory的api为指定的目标对象创建代理对象, 并指定增强为SsoSecurityAdapter, 其中SsoSecurityAdapter增强类型为MethodInterceptor类型
   if (this.configType.isAssignableFrom(bean.getClass()) && bean instanceof WebSecurityConfigurerAdapter) {
       
      // 代理工厂
      ProxyFactory factory = new ProxyFactory();
      
      // 被代理的目标对象
      factory.setTarget(bean);
      
      // 指定增强, 可指定多个增强, 会形成1条增强链
      // 其中增强为: SsoSecurityAdapter, 具体逻辑看下面
      factory.addAdvice(new SsoSecurityAdapter(this.applicationContext));
      
      // 使用代理工厂中创建代理对象, 代理对象将替代掉原来的bean而放入spring容器中
      bean = factory.getProxy();
   }
   return bean;
}

SsoSecurityAdapter
SsoSecurityAdapter其实就是个MethodInterceptor的增强类型对象,在构造方法中就创建了SsoSecurityConfigurer配置器,这个配置器是用来配置HttpSecurity的。
SsoSecurityConfigurer配置器就是给HttpSecurity这个过滤器链中添加OAuth2ClientAuthenticationProcessingFilter过滤器和修改异常处理的配置。
SsoSecurityAdapter的目的就是在WebSecurityConfigurerAdapter的子类在执行WebSecurityConfigurerAdapter类中的init(WebSecurity)方法时,先调用WebSecurityConfigurerAdapter类的getHttp(),等到WebSecurityConfigurerAdapter将HttpSecurity创建好,并且给创建好的HttpSecurity添加完配置器后,再使用SsoSecurityAdapter构造方法中所创建SsoSecurityConfigurer来配置这个HttpSecurity,security在这里特意搞个动态代理做这件事,就是要让我们特别注意这个代理的执行时机。至于SsoSecurityConfigurer怎么配置的HttpSecurity,接着往下看

// MethodInterceptor
private static class SsoSecurityAdapter implements MethodInterceptor {

   // 构造方法中初始化
   private SsoSecurityConfigurer configurer;

   // applicationContext由OAuth2SsoCustomConfiguration实现ApplicationContextAware接口, 传递而来
   SsoSecurityAdapter(ApplicationContext applicationContext) {
       
      // 创建SsoSecurityConfigurer, 用来往配置和修改HttpSecurity的
      this.configurer = new SsoSecurityConfigurer(applicationContext);
   }

   /* 增强逻辑 */
   @Override
   public Object invoke(MethodInvocation invocation)  {
       
      // 当WebSecurityConfigurerAdapter的init(WebSecurity)方法调用时
      if (invocation.getMethod().getName().equals("init")) {
          
         // 找到WebSecurityConfigurerAdapter的getHttp()方法
         Method method = ReflectionUtils.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
         
         // 使用反射 调用WebSecurityConfigurerAdapter的getHttp()
         ReflectionUtils.makeAccessible(method);
         HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method, invocation.getThis());
         
         // 调用WebSecurityConfigurerAdapter的getHttp()后, 
         // 此时, HttpSecurity已经走完WebSecurityConfigurerAdapter对它的配置了, 现在再来使用SsoSecurityConfigurer配置HttpSecurity
         this.configurer.configure(http);
      }
      
      // 接着走 WebSecurityConfigurerAdapter的init()方法, 上面不是已经走了一边吗? 为什么还要再走一遍? 
      // 因为init方法的后面一步: 将HttpSecurity作为过滤器链添加到WebSecurity的securityFilterChainBuilders属性中还没有走完,
      // 并且getHttp()方法中判断如果HttpSecurity不为空, 因此再走一遍getHttp()的逻辑是没问题的
      return invocation.proceed();
   }

}

WebSecurityConfigurerAdapter的init方法如下

public void init(final WebSecurity web) throws Exception {
    
   // 第一步: 先获取HttpSecurity, 并且给HttpSecurity添加配置器
   final HttpSecurity http = getHttp();
   
   // 第二步: 将HttpSecurity作为过滤器链添加到WebSecurity的securityFilterChainBuilders属性中, 
   // 只不过此时的HttpSecurity中只添加了配置, 还要等WebSecurity调用performBuild()方法时, 再触发HttpSecurity的build()方法, 此时才开始HttpSecurity的配置器生效
   web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
      // 等到所有HttpSecurity的过滤器链都配置并构建好了, WebSecurity的FilterChainProxy也构建好了, 
      // 则将过滤器链中的FilterSecurityInterceptor设置到WebSecurity中, 就是保存下FilterSecurityInterceptor这个对象
      FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
      web.securityInterceptor(securityInterceptor);
   });
}
SsoSecurityConfigurer
configure(HttpSecurity)方法
public void configure(HttpSecurity http) {
    
    // 获取OAuth2SsoProperties这个bean, 它的配置前缀是: security.oauth2.sso, 唯一可配置项就是: security.oauth2.sso.loginPath = /login
    OAuth2SsoProperties sso = this.applicationContext.getBean(OAuth2SsoProperties.class);
    
    // 第一步: 创建【OAuth2ClientAuthenticationProcessingFilter】过滤器, 这个过滤器是实现SSO单点登录的最重要的过滤器之一, 
    //        它内部会对授权服务发起请求获取OAuth2AccessToken令牌对象, 然后使用ResourceServerTokenServices读取令牌而载入OAuth2Authentication作为认证对象,
    //        这就是OAuth2ClientAuthenticationProcessingFilter过滤器的认证逻辑
    // 第二步: 给HttpSecurity添加OAuth2ClientAuthenticationConfigurer配置器, 配置器中传入了OAuth2ClientAuthenticationProcessingFilter
    //         这很显然OAuth2ClientAuthenticationConfigurer配置器就是给HttpSecurity中添加OAuth2ClientAuthenticationProcessingFilter这个过滤器的
    http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso)));
    
    // 给HttpSecurity的ExceptionHandlingConfigurer的defaultEntryPointMappings这个map中添加了2个入口处理:
    // 1. ACCEPT请求头对应的值是: application/xhtml+xml、image/*、text/html、text/plain时, 将浏览器重定向到security.oauth2.sso.loginPath所指向的路径
    // 2. X-Requested-With请求头对应的值是: XMLHttpRequest时, 即为ajax请求时, 返回401状态码
    addAuthenticationEntryPoint(http, sso);
}
oauth2SsoFilter(OAuth2SsoProperties)方法

这个方法就是用来创建OAuth2ClientAuthenticationProcessingFilter过滤器用的,并且给这个过滤器填充相关的属性

private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(OAuth2SsoProperties sso) {

    // 这个【UserInfoRestTemplateFactory】默认定义在ResourceServerTokenServicesConfiguration配置类中,
    // 它的作用就是根据OAuth2协议, 发起请求获取访问令牌
    OAuth2RestOperations restTemplate = this.applicationContext.getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
    
    // 这个【ResourceServerTokenServices】默认定义在ResourceServerTokenServicesConfiguration配置类中,
    // 它的作用就是根据访问令牌获取OAuth2Authentication认证对象
    ResourceServerTokenServices tokenServices = this.applicationContext.getBean(ResourceServerTokenServices.class);
    
    // 创建【OAuth2ClientAuthenticationProcessingFilter】过滤器, 并传入security.oauth2.sso.loginPath配置的路径, 默认为: /login
    // 前面提到会给ExceptionHandlingConfigurer添加的第1个入口处理, 会让浏览器重定向到/login, 这里的这个过滤器处理的认证请求路径正好是: /login
    // 因此, 当异常处理过滤器将浏览器重定向到/login时, 该OAuth2ClientAuthenticationProcessingFilter处理/login时, 会交给OAuth2RestTemplate获取访问令牌, 
    //      OAuth2RestTemplate会交给AuthorizationCodeAccessTokenProvider授权码处理, 由于当前没有授权码, 
    //      因此在AuthorizationCodeAccessTokenProvider的obtainAccessToken(..)方法中会抛出UserRedirectRequiredException异常, 异常里面会包含认证中心路径和state信息,
    //      这个UserRedirectRequiredException异常会被web容器的OAuth2ClientContextFilter过滤器处理,这个过滤器会读取异常中的信息, 然后将浏览器重定向到认证中心路径 
    //      等用户在认证中心登录成功后, 会跳转到当前系统/login?code=xxx&state=yyy路径, 并在路径携带code和state参数了, 
    //      此时仍然交给OAuth2ClientAuthenticationProcessingFilter过滤器处理, 
    //      因为携带了code授权码, 根据OAuth2协议, 就可以向认证中心请求访问令牌了, 因此就不会抛出UserRedirectRequiredException异常了,
    //      拿到令牌之后, 就可以使用ResourceServerTokenServices得到用户信息了, 这样就完整了用户认证过程
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(sso.getLoginPath());
    
    // 填充过滤器的restTemplate属性, 用于获取访问令牌
    filter.setRestTemplate(restTemplate);
    
    // 填充过滤器的tokenServices属性, 用于根据访问令牌加载OAuth2Authentication对象
    filter.setTokenServices(tokenServices);
    
    // 填充过滤器的eventPublisher属性, 用于在认证成功或认证失败时, 发布对应的事件
    filter.setApplicationEventPublisher(this.applicationContext);
    
    return filter;
}

ResourceServerTokenServicesConfiguration

使用@EnableOAuth2Sso注解时,会使用@Import引入ResourceServerTokenServicesConfiguration这个配置类;

OAuth2AutoConfiguration自动配置类会使用@Import引入OAuth2ResourceServerConfiguration配置类时,OAuth2ResourceServerConfiguration配置类也会使用@Import引入ResourceServerTokenServicesConfiguration这个配置类,当然,这必须在开启资源服务器的条件下才会生效;

这个配置类的的主要目的有2个:

第1个是定义1个UserInfoRestTemplateFactory作为bean,提供给SsoSecurityConfigurer配置器中创建OAuth2ClientAuthenticationProcessingFilter时,获取UserInfoRestTemplateFactory这个bean,来得到OAuth2RestTemplate设置到OAuth2ClientAuthenticationProcessingFilter认证过滤器中,在认证过滤器中会使用该OAuth2RestTemplate发起通过授权码获取token的请求;

第2个是根据配置来决定使用1个什么样的ResourceServerTokenServices的实现,在OAuth2ClientAuthenticationProcessingFilter认证过滤器中使用OAuth2RestTemplate得到token后,就会交给ResourceServerTokenServices去根据获取的此token加载出OAuth2Authentication认证对象,那么加载就可以是通过远程调用授权服务器获取,也可以是本地通过读取符合jwt规范的token令牌来获取。

@Bean
@ConditionalOnMissingBean
public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
        ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers, // 可自定义UserInfoRestTemplateCustomizer实现, 这里会自动注入
        ObjectProvider<OAuth2ProtectedResourceDetails> details,           // 在OAuth2AutoConfiguration自动配置类中引入的OAuth2RestOperationsConfiguration中有定义
        ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {        // 如果使用了@EnableOAuth2Client, 那么定义在OAuth2ClientConfiguration配置类中的oauth2ClientContext的bean就会生效
                                                                          // 在OAuth2AutoConfiguration自动配置类中引入的OAuth2RestOperationsConfiguration中也有oauth2ClientContext的bean的定义
    return new DefaultUserInfoRestTemplateFactory(customizers, details,oauth2ClientContext);
}
RemoteTokenServicesConfiguration

该配置类生效的条件是:JwtTokenCondition,JwkCondition,JwtKeyStoreCondition这3个条件都不满足时,才会生效

TokenInfoServicesConfiguration

RemoteTokenServicesConfiguration内部又定义了TokenInfoServicesConfiguration配置类,如下:

@Configuration
@Conditional(TokenInfoCondition.class) // security.oauth2.resource.prefer-token-info配置为true或默认不填        
                                       // 必须配置security.oauth2.resource.token-info-uri, 可选配置security.oauth2.resource.user-info-uri

protected static class TokenInfoServicesConfiguration {

    // ResourceServerProperties在OAuth2AutoConfiguration中以@Bean的方式配置
    // 它的配置前缀为: security.oauth2.resource, 配置属性有: clientId、clientSecret、serviceId、id、userInfoUri、tokenInfoUri、
    // tokenInfoUri、tokenType、jwt.keyValue、jwt.keyUri、jwt.keyStore、jwt.keyStorePassword、jwt.keyAlias、jwt.keyPassword、jwk.keySetUri,
    // 其中tokenType为Bearer, clientId和clientSecret直接取OAuth2ClientProperties配置的clientId和clientSecret; OAuth2ClientProperties的配置前缀是: security.oauth2.client
    private final ResourceServerProperties resource;
    
    protected TokenInfoServicesConfiguration(ResourceServerProperties resource) {
        this.resource = resource;
    }
    
    /* RemoteTokenServices将携带token访问令牌, 通过发请求给认证中心, 来载入OAuth2Authentication认证对象 */
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        RemoteTokenServices services = new RemoteTokenServices();
        
        // 用的是CheckEndpoint校验令牌端点的路径, 是通过security.oauth2.resource.token-info-uri来配置的
        services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
        
        // 客户端id由security.oauth2.resource.client-id来配置
        services.setClientId(this.resource.getClientId());
        
        // 客户端密码由security.oauth2.resource.client-secret来配置
        services.setClientSecret(this.resource.getClientSecret());
        
        return services;
    }

}
SocialTokenServicesConfiguration

需要引入spring-social的依赖,基本上没看见有人用这个,因此,就简单的看下

@Configuration
@ConditionalOnClass(OAuth2ConnectionFactory.class) // 必须引入spring-social的依赖
@Conditional(NotTokenInfoCondition.class) // 不能配置security.oauth2.resource.token-info-uri
protected static class SocialTokenServicesConfiguration {
    
   // ResourceServerProperties在OAuth2AutoConfiguration中以@Bean的方式配置(上面有提及)
   private final ResourceServerProperties sso;

   private final OAuth2ConnectionFactory<?> connectionFactory;

   private final OAuth2RestOperations restTemplate;

   private final AuthoritiesExtractor authoritiesExtractor;

   private final PrincipalExtractor principalExtractor;

   public SocialTokenServicesConfiguration(ResourceServerProperties sso,
         ObjectProvider<OAuth2ConnectionFactory<?>> connectionFactory,
         UserInfoRestTemplateFactory restTemplateFactory,
         ObjectProvider<AuthoritiesExtractor> authoritiesExtractor,
         ObjectProvider<PrincipalExtractor> principalExtractor) {
      this.sso = sso;
      this.connectionFactory = connectionFactory.getIfAvailable();
      this.restTemplate = restTemplateFactory.getUserInfoRestTemplate();
      this.authoritiesExtractor = authoritiesExtractor.getIfAvailable();
      this.principalExtractor = principalExtractor.getIfAvailable();
   }

   @Bean
   @ConditionalOnBean(ConnectionFactoryLocator.class) // 当存在ConnectionFactoryLocator这个bean时才生效
   @ConditionalOnMissingBean(ResourceServerTokenServices.class) // 当不存在ResourceServerTokenServices这个bean时才生效
   public SpringSocialTokenServices socialTokenServices() {
       // SpringSocialTokenServices实现了ResourceServerTokenServices, 它需要1个ConnectionFactoryLocator的bean
      return new SpringSocialTokenServices(this.connectionFactory, this.sso.getClientId());
   }

   @Bean
   @ConditionalOnMissingBean({ ConnectionFactoryLocator.class,      // 当不存在ConnectionFactoryLocator这个bean时才生效(与上面正好相反)
                               ResourceServerTokenServices.class }) // 当不存在ResourceServerTokenServices这个bean时才生效(都必须不存在ResourceServerTokenServices这个bean)
   public UserInfoTokenServices userInfoTokenServices() {
       // UserInfoTokenServices也是通过发请求给认证中心获取用户信息, 通过security.oauth2.resource.user-info-uri来配置请求路径
      UserInfoTokenServices services = new UserInfoTokenServices(this.sso.getUserInfoUri(), this.sso.getClientId());
      services.setTokenType(this.sso.getTokenType());
      services.setRestTemplate(this.restTemplate);
      
      // 发完请求后, authoritiesExtractor用来提取 权限信息
      if (this.authoritiesExtractor != null) {
         services.setAuthoritiesExtractor(this.authoritiesExtractor);
      }
      
      // 发完请求后, principalExtractor用来提取 用户信息
      if (this.principalExtractor != null) {
         services.setPrincipalExtractor(this.principalExtractor);
      }
      return services;
   }

}
JwtTokenCondition
@Configuration
@Conditional(JwtTokenCondition.class) // 生效条件是必须配置: security.oauth2.resource.jwt.key-value或者security.oauth2.resource.jwt.key-uri 作为公钥
protected static class JwtTokenServicesConfiguration {

    // ResourceServerProperties在OAuth2AutoConfiguration中以@Bean的方式配置(上面有提及)
    private final ResourceServerProperties resource;
    
    // 可以自定义JwtAccessTokenConverterConfigurer来定制修改JwtAccessTokenConverter令牌转换器
    private final List<JwtAccessTokenConverterConfigurer> configurers;
    
    // 可以自定义JwtAccessTokenConverterRestTemplateCustomizer来定制修改RestTemplate
    private final List<JwtAccessTokenConverterRestTemplateCustomizer> customizers;
    
    // 完成它们的自动注入
    public JwtTokenServicesConfiguration(ResourceServerProperties resource,
                                         ObjectProvider<List<JwtAccessTokenConverterConfigurer>> configurers,
                                         ObjectProvider<List<JwtAccessTokenConverterRestTemplateCustomizer>> customizers) {
        this.resource = resource;
        this.configurers = configurers.getIfAvailable();
        this.customizers = customizers.getIfAvailable();
    }
    
    /* 定义了 DefaultTokenServices 这个bean */
    @Bean
    @ConditionalOnMissingBean(ResourceServerTokenServices.class)
    public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) { // 引入TokenStore 令牌存储器
        DefaultTokenServices services = new DefaultTokenServices();
         // 给DefaultTokenServices设置TokenStore
        services.setTokenStore(jwtTokenStore);
        return services;
    }
    
    /* 定义了 TokenStore 这个bean */
    @Bean
    @ConditionalOnMissingBean(TokenStore.class)
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer()); // 引用了 JwtAccessTokenConverter 令牌增强器
    }
    
    /* 定义了 JwtAccessTokenConverter 这个bean */
    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        
        // 创建jwt令牌转换器
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        
        // 通过security.oauth2.resource.jwt.keyValue来配置
        String keyValue = this.resource.getJwt().getKeyValue();
        
        // 如果security.oauth2.resource.jwt.keyValue没有配置
        if (!StringUtils.hasText(keyValue)) {
            // 既然这里没有配置具体的公钥, 那就要配置获取公钥的地址, 默认配置: security.oauth2.resource.jwt.key-uri
            // 在认证服务中 TokenKeyEndpoint 来提供该公钥
            keyValue = getKeyFromServer();
        }
        
        // 如果不是以"-----BEGIN"开头,则说明是对称加密,设置为签名密钥
        if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) {
            converter.setSigningKey(keyValue);
        }

        if (keyValue != null) {
            // 设置校验所需要的key, 设置JwtAccessTokenConverter#verifierKey属性后,
            // 会调用JwtAccessTokenConverter#afterPropertiesSet()方法(里面有使用的示例)
            converter.setVerifierKey(keyValue);
        }
        
        // 对所有的JwtAccessTokenConverterConfigurer配置器进行排序, 来定制修改JwtAccessTokenConverter
        if (!CollectionUtils.isEmpty(this.configurers)) {
            AnnotationAwareOrderComparator.sort(this.configurers);
            for (JwtAccessTokenConverterConfigurer configurer : this.configurers) {
                configurer.configure(converter);
            }
        }
        
        return converter;
    }
    
    private String getKeyFromServer() {
        
        // 创建RestTemplate, 然后使用JwtAccessTokenConverterRestTemplateCustomizer对该RestTemplate进行修改
        RestTemplate keyUriRestTemplate = new RestTemplate();
        if (!CollectionUtils.isEmpty(this.customizers)) {
            for (JwtAccessTokenConverterRestTemplateCustomizer customizer : this.customizers) {
                customizer.customize(keyUriRestTemplate);
            }
        }
        
        // 请求tokenKey之前, 添加Authorization头, 值为: Basic + 空格 + base64({security.oauth2.resource.client-id}:{security.oauth2.resource:client-secret})
        HttpHeaders headers = new HttpHeaders();
        String username = this.resource.getClientId();
        String password = this.resource.getClientSecret();
        if (username != null && password != null) {
            byte[] token = Base64.getEncoder().encode((username + ":" + password).getBytes());
            headers.add("Authorization", "Basic " + new String(token));
        }
        HttpEntity<Void> request = new HttpEntity<>(headers);
        
        // 由security.oauth2.resource.key-uri配置
        // 在授权服务中由TokenKeyEndpoint处理该请求
        String url = this.resource.getJwt().getKeyUri();
        
        return (String) keyUriRestTemplate.exchange(url, HttpMethod.GET, request, Map.class).getBody().get("value");
    }

}
JwkTokenStoreConfiguration

@Configuration
@Conditional(JwkCondition.class)
protected static class JwkTokenStoreConfiguration {

    // ResourceServerProperties在OAuth2AutoConfiguration中以@Bean的方式配置(上面有提及)
    private final ResourceServerProperties resource;
    
    public JwkTokenStoreConfiguration(ResourceServerProperties resource) {
        this.resource = resource;
    }
    
    /* 定义了 DefaultTokenServices 这个bean */
    @Bean
    @ConditionalOnMissingBean(ResourceServerTokenServices.class)
    public DefaultTokenServices jwkTokenServices(TokenStore jwkTokenStore) { // 引入TokenStore 令牌存储器
        DefaultTokenServices services = new DefaultTokenServices();
        // 给DefaultTokenServices设置TokenStore
        services.setTokenStore(jwkTokenStore);
        return services;
    }
    
    /* 定义了 TokenStore 这个bean */
    @Bean
    @ConditionalOnMissingBean(TokenStore.class)
    public TokenStore jwkTokenStore() {
        // 由security.oauth2.resource.jwk.key-set-uri
        return new JwkTokenStore(this.resource.getJwk().getKeySetUri());
    }
}
JwtKeyStoreConfiguration
@Configuration
@Conditional(JwtKeyStoreCondition.class)
protected class JwtKeyStoreConfiguration implements ApplicationContextAware {

   /* ResourceServerProperties在OAuth2AutoConfiguration中以@Bean的方式配置(上面有提及) */
   private final ResourceServerProperties resource;
   
   private ApplicationContext context;

   @Autowired  /* 自动注入ResourceServerProperties */
   public JwtKeyStoreConfiguration(ResourceServerProperties resource) {
      this.resource = resource;
   }

   @Override /* 自动注入ApplicationContext */
   public void setApplicationContext(ApplicationContext context) throws BeansException {
      this.context = context;
   }

   /* 定义了 DefaultTokenServices 这个bean */
   @Bean
   @ConditionalOnMissingBean(ResourceServerTokenServices.class)
   public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) {// 引入TokenStore 令牌存储器
      DefaultTokenServices services = new DefaultTokenServices();
      // 给DefaultTokenServices设置TokenStore
      services.setTokenStore(jwtTokenStore);
      return services;
   }

   /* 定义了 TokenStore 这个bean */
   @Bean
   @ConditionalOnMissingBean(TokenStore.class)
   public TokenStore tokenStore() {
      return new JwtTokenStore(accessTokenConverter()); // 引入JwtAccessTokenConverter 令牌转换器
   }

   @Bean
   public JwtAccessTokenConverter accessTokenConverter() {
       
      // 必须配置: security.oauth2.resource.jwt.key-store
      // 必须配置: security.oauth2.resource.jwt.key-store-password
      // 必须配置: security.oauth2.resource.jwt.key-alias
      Assert.notNull(this.resource.getJwt().getKeyStore(), "keyStore cannot be null");
      Assert.notNull(this.resource.getJwt().getKeyStorePassword(), "keyStorePassword cannot be null");
      Assert.notNull(this.resource.getJwt().getKeyAlias(), "keyAlias cannot be null");

      JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

      // 获取security.oauth2.resource.jwt.key-store所指向的文件
      Resource keyStore = this.context.getResource(this.resource.getJwt().getKeyStore());
      // 获取security.oauth2.resource.jwt.key-store-password配置的密钥
      char[] keyStorePassword = this.resource.getJwt().getKeyStorePassword().toCharArray();
      // 根据 文件 + 密钥 得到=> KeyStoreKeyFactory
      KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyStore, keyStorePassword);
        
      // 获取security.oauth2.resource.jwt.key-alias配置的别名
      String keyAlias = this.resource.getJwt().getKeyAlias();
      // 优先获取 security.oauth2.resource.key-password, 如果没有配置, 则取获取 security.oauth2.resource.jwt.key-store-password 作为 keyPassword
      char[] keyPassword = Optional.ofNullable(this.resource.getJwt().getKeyPassword()).map(String::toCharArray).orElse(keyStorePassword);
      
      // 使用 KeyStoreKeyFactory 根据 keyAlias + keyPassword 得到密钥对, 设置给令牌转换器
      converter.setKeyPair(keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword));

      return converter;
   }
}

OAuth2AutoConfiguration

OAuth2AutoConfiguration自动配置类,由springboot启动时自动加载;
它使用@Import引入了4个配置类:OAuth2AuthorizationServerConfiguration、OAuth2MethodSecurityConfiguration、OAuth2ResourceServerConfiguration、OAuth2RestOperationsConfiguration;

它开启了1个配置类:OAuth2ClientProperties,配置前缀为:security.oauth2.client,可配置的属性包括:clientId、clientSecret、clientSecret,显然就是用来配置OAuth2客户端的;

它里面定义了1个ResourceServerProperties的bean,配置前缀为:security.oauth2.resource,可配置的属性包括:clientId、clientSecret、serviceId、id、userInfoUri、tokenInfoUri、tokenInfoUri、tokenType、jwt.keyValue、jwt.keyUri、jwt.keyStore、jwt.keyStorePassword、jwt.keyAlias、jwt.keyPassword、jwk.keySetUri,其中clientId和clientSecret直接取OAuth2ClientProperties配置的clientId和clientSecret,tokenType为Bearer;

OAuth2AuthorizationServerConfiguration

它继承自AuthorizationServerConfigurerAdapter,这个得使用@EnableAuthorizationServer注解开启授权服务器才会生效,因为当前是SSO客户端,未开启授权服务器,因此该配置类不会生效。

OAuth2MethodSecurityConfiguration

这个得使用@EnableGlobalMethodSecurity注解开启方法保护才会生效

OAuth2ResourceServerConfiguration

这个得使用@EnableResourceServer注解开启方法保护才会生效

OAuth2RestOperationsConfiguration

这个的生效条件为,存在EnableOAuth2Client这个类即可,显然,在单点登录客户端中,这个类当然存在
它内部定义了3个内部静态类作为配置类:SingletonScopedConfiguration、SessionScopedConfiguration、RequestScopedConfiguration

SingletonScopedConfiguration
  1. 生效条件是security.oauth2.client.grant-type必须配置为client_credentials, 否则不匹配
  2. 定义了ClientCredentialsResourceDetails作为OAuth2ProtectedResourceDetails
  3. 定义了 OAuth2ClientContext 客户端上下文作为bean
@Configuration
@Conditional(ClientCredentialsCondition.class) // 生效条件是security.oauth2.client.grant-type必须配置为client_credentials, 否则不匹配    
protected static class SingletonScopedConfiguration {

    /* 定义了OAuth2ProtectedResourceDetails 受保护的资源服务配置 */
    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    @Primary
    public ClientCredentialsResourceDetails oauth2RemoteResource() {
        // ClientCredentialsResourceDetails继承自BaseOAuth2ProtectedResourceDetails, grantType属性默认设置为grantType
        ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
        return details;
    }
    
    /* 定义了 OAuth2ClientContext 客户端上下文 */
    @Bean
    public DefaultOAuth2ClientContext oauth2ClientContext() {
        return new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
    }

}
SessionScopedConfiguration
  1. 生效条件是容器中必须存在OAuth2ClientConfiguration这个bean, 否则不匹配
  2. 生效条件必须满足配置了security.oauth2.client.client-id,并且security.oauth2.client.grant_type的值不为client_credentials,也就是这个配置类与SingletonScopedConfiguration互斥
  3. 引入AuthorizationCodeResourceDetails作为OAuth2ProtectedResourceDetails
  4. 将OAuth2ClientConfiguration中定义的OAuth2ClientContextFilter过滤器,以FilterRegistrationBean的形式添加到Web容器中,并指定顺序比FilterChainProxy所包装的DelegatingFilterProxy靠前
@Configuration
@ConditionalOnBean(OAuth2ClientConfiguration.class) // 容器中必须存在OAuth2ClientConfiguration这个bean, 
                                                    // 只要是使用@EnableOAuth2Client注解, 就会引入OAuth2ClientConfiguration这个bean, 
                                                    // 而@EnableOAuth2Sso就会引入@EnableOAuth2Client注解
@Conditional({ OAuth2ClientIdCondition.class,                  // 是否有配置: security.oauth2.client.client-id 对应的值, 有配置该配置对应的值, 则匹配, 否则不匹配
               NoClientCredentialsCondition.class })           // security.oauth2.client.grant_type值不为client_credentials, 如果该值未明确指定该配置的值为client_credentials, 那么这个条件会满足
               
@Import(OAuth2ProtectedResourceDetailsConfiguration.class)     // 引入了OAuth2ProtectedResourceDetailsConfiguration配置类, 
                                                               // 该配置类中定义了AuthorizationCodeResourceDetails这个bean并作为primary, 默认grantType写死为: authorization_code,
                                                               // 并且会读取security.oauth2.client下的配置属性, 这些属性都定义在BaseOAuth2ProtectedResourceDetails中, 其中包括:
                                                               // id、grantType、clientId、accessTokenUri、scope、clientSecret、clientAuthenticationScheme、authorizationScheme、tokenName                                                                                                                                               
protected static class SessionScopedConfiguration {

    // 自动注入OAuth2ClientConfiguration中定义的OAuth2ClientContextFilter, 
    // 自动注入以spring.security为配置前缀的SecurityProperties, 可配置项有: filter.order、filter.dispatcherTypes、user.name、user.password、user.roles、user.passwordGenerated
    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter, SecurityProperties security) {
        FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(filter);
        
        // 设定OAuth2ClientContextFilter的顺序, 默认为: -110
        registration.setOrder(security.getFilter().getOrder() - 10);
        return registration;
    }
    
}
RequestScopedConfiguration
@Configuration
@ConditionalOnMissingBean(OAuth2ClientConfiguration.class) // 容器中不存在OAuth2ClientConfiguration这个bean时, 才会生效
@Conditional({ OAuth2ClientIdCondition.class,              // 是否有配置: security.oauth2.client.client-id 对应的值, 有配置该配置对应的值, 则匹配, 否则不匹配(与SessionScopedConfiguration一致)
               NoClientCredentialsCondition.class          // security.oauth2.client.grant_type值不为client_credentials, 如果该值未明确指定该配置的值为client_credentials, 那么这个条件会满足(与SessionScopedConfiguration一致)
})
@Import(OAuth2ProtectedResourceDetailsConfiguration.class) // 引入了OAuth2ProtectedResourceDetailsConfiguration配置类(与SessionScopedConfiguration一致), 
                                                           // 该配置类中定义了AuthorizationCodeResourceDetails这个bean并作为primary, 默认grantType写死为: authorization_code,
                                                           // 并且会读取security.oauth2.client下的配置属性, 这些属性都定义在BaseOAuth2ProtectedResourceDetails中, 其中包括:
                                                           // id、grantType、clientId、accessTokenUri、scope、clientSecret、clientAuthenticationScheme、authorizationScheme、tokenName    
protected static class RequestScopedConfiguration {

    /* 定义了 OAuth2ClientContext 这个bean, 并且指定它的域为 request, 
       那么针对每次请求, 都会是1个新的DefaultOAuth2ClientContext, 因此, 这个类的名称叫作 RequestScopedConfiguration,
       而SessionScopedConfiguration则是必须依赖OAuth2ClientConfiguration中定义的OAuth2ClientContext, 它的域是session, 所以类名叫作SessionScopedConfiguration */
    @Bean
    @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
    public DefaultOAuth2ClientContext oauth2ClientContext() {
        
        // 创建DefaultAccessTokenRequest, 并传入DefaultOAuth2ClientContext, 注意: 每次请求都会创建1个新的DefaultAccessTokenRequest和1个新的DefaultOAuth2ClientContext
        DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
        
        // 因为每次请求时, 调用由该@Bean方法所暴露的oauth2ClientContext这个代理对象的方法时,  都会调用当前@Bean方法, 因此这里能尝试去拿绑定到线程中的认证对象
        Authentication principal = SecurityContextHolder.getContext().getAuthentication();
        
        // 从绑定到当前线程的OAuth2Authentication认证对象中, 获取它的details的tokenValue值, 将的tokenValue设置到DefaultOAuth2ClientContext中
        if (principal instanceof OAuth2Authentication) {
            OAuth2Authentication authentication = (OAuth2Authentication) principal;
            Object details = authentication.getDetails();
            if (details instanceof OAuth2AuthenticationDetails) {
                OAuth2AuthenticationDetails oauthsDetails = (OAuth2AuthenticationDetails) details;
                String token = oauthsDetails.getTokenValue();
                context.setAccessToken(new DefaultOAuth2AccessToken(token));
            }
        }
        
        return context;
    }

}

过滤器

在这里插入图片描述

过滤器的顺序早已约定FilterComparator

这在HttpSecurity的performBuild()方法中,就会对filters属性中所添加的所有过滤器进行排序,排序的规则就是使用的是FilterComparator

final class FilterComparator implements Comparator<Filter>, Serializable {
   private static final int INITIAL_ORDER = 100;
   private static final int ORDER_STEP = 100;
   private final Map<String, Integer> filterToOrder = new HashMap<>();

   FilterComparator() {
      Step order = new Step(INITIAL_ORDER, ORDER_STEP);
      put(ChannelProcessingFilter.class, order.next());
      put(ConcurrentSessionFilter.class, order.next());
      put(WebAsyncManagerIntegrationFilter.class, order.next());
      put(SecurityContextPersistenceFilter.class, order.next());
      put(HeaderWriterFilter.class, order.next());
      put(CorsFilter.class, order.next());
      put(CsrfFilter.class, order.next());
      put(LogoutFilter.class, order.next());
      filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());
      filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next());
      put(X509AuthenticationFilter.class, order.next());
      put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
      filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
      filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());
      filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next());
      put(UsernamePasswordAuthenticationFilter.class, order.next());
      put(ConcurrentSessionFilter.class, order.next());
      filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
      put(DefaultLoginPageGeneratingFilter.class, order.next());
      put(DefaultLogoutPageGeneratingFilter.class, order.next());
      put(ConcurrentSessionFilter.class, order.next());
      put(DigestAuthenticationFilter.class, order.next());
      filterToOrder.put("org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
      put(BasicAuthenticationFilter.class, order.next());
      put(RequestCacheAwareFilter.class, order.next());
      put(SecurityContextHolderAwareRequestFilter.class, order.next());
      put(JaasApiIntegrationFilter.class, order.next());
      put(RememberMeAuthenticationFilter.class, order.next());
      put(AnonymousAuthenticationFilter.class, order.next());
      filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());
      put(SessionManagementFilter.class, order.next());
      put(ExceptionTranslationFilter.class, order.next());
      put(FilterSecurityInterceptor.class, order.next());
      put(SwitchUserFilter.class, order.next());
   }

OAuth2ClientContextFilter

该过滤器应用在Security的FilterChainProxy的过滤器链的前面,当security中的过滤器链中的OAuth2ClientAuthenticationProcessingFilter抛出UserRedirectRequiredException异常后,由该OAuth2ClientContextFilter捕获该异常,得到该异常中的数据,组装重定向url,并将浏览器重定向到认证中心(具体过程在oauth2SsoFilter(OAuth2SsoProperties)中已详述)

OAuth2ClientAuthenticationProcessingFilter

单点登录的核心处理过滤器,当用户重定向到认证服务器后,登录成功后,将会携带code和state重定向到我们的服务,此时由当前的这个OAuth2ClientAuthenticationProcessingFilter过滤器处理,它会交给OAuth2RestTemplate携带授权码请求访问令牌,然后交给ResourceServerTokenServices携带访问令牌获取用户信息,从而加载OAuth2Authentication认证对象绑定到当前线程。

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

   OAuth2AccessToken accessToken;
   try {
       // 这里的restTemplate由@EnableOAuth2Sso注解使用@Import注解引入的ResourceServerTokenServicesConfiguration配置类中定义的UserInfoRestTemplateFactory这个bean,
       // 在SsoSecurityConfigurer的oauth2SsoFilter(..)方法中获取该UserInfoRestTemplateFactory的bean, 使用它获取OAuth2RestOperations, 设置给OAuth2ClientAuthenticationProcessingFilter,
       // 然后由OAuth2RestOperations去获取访问令牌, 这个获取令牌比较重要, 下面对它展开分析
      accessToken = restTemplate.getAccessToken();
   } catch (OAuth2Exception e) {
      BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
      publish(new OAuth2AuthenticationFailureEvent(bad));
      throw bad;       
   }
   
   try {
      // 1. 这里的tokenServices由@EnableOAuth2Sso注解使用@Import注解引入的ResourceServerTokenServicesConfiguration配置类中定义的ResourceServerTokenServices的bean, 
      //    这个ResourceServerTokenServices的bean需要根据配置来确定使用哪一种, 这个方法在【OAuth2授权流程和源码解析】中已详述;
      // 2. 在案例中如果我们配置了: security.oauth2.resource.user-info-uri=http://localhost:1110/user, 这个/user的请求是在授权服务中自己创建UserController实现的, 
      //    此时ResourceServerTokenServicesConfiguration中生效的配置是: UserInfoTokenServicesConfiguration, 此时这里的tokenServices实现是: UserInfoTokenServices,
      //    而定义UserInfoTokenServices这个bean时, 所设置的restTemplate正好就是从容器中注入UserInfoRestTemplateFactory这个bean的getUserInfoRestTemplate()方法获取的,
      //    这一点可以在UserInfoTokenServicesConfiguration配置类的userInfoTokenServices()方法中得知, 并且UserInfoRestTemplateFactory的getUserInfoRestTemplate()方法返回
      //    在返回OAuth2RestTemplate时的逻辑是, 之前如果之前调用过这个方法, 就把之前创建的OAuth2RestTemplate返回, 因此UserInfoTokenServices中使用的OAuth2RestTemplate与
      //    OAuth2ClientAuthenticationProcessingFilter中所使用的OAuth2RestTemplate其实是同一个, 
      //    就是说: 上面使用restTemplate.getAccessToken()与tokenServices中使用的restTemplate是同一个
      OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
      if (authenticationDetailsSource!=null) {
         // key为: "OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE"
         request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
         // key为: "OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE"
         request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
         result.setDetails(authenticationDetailsSource.buildDetails(request));
      }
      publish(new AuthenticationSuccessEvent(result));
      return result;
   }
   catch (InvalidTokenException e) {
      BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
      publish(new OAuth2AuthenticationFailureEvent(bad));
      throw bad;       
   }

}

OAuth2RestTemplate

OAuth2RestTemplate由@EnableOAuth2Sso注解使用@Import注解引入的ResourceServerTokenServicesConfiguration配置类中定义的UserInfoRestTemplateFactory这个bean,在SsoSecurityConfigurer的oauth2SsoFilter(…)方法中获取该UserInfoRestTemplateFactory的bean, 使用它获取OAuth2RestOperations, 设置给OAuth2ClientAuthenticationProcessingFilter。

有2个作用,第1个是:向认证中心获取访问令牌;第2个是:携带访问令牌,获取用户信息;

// 1. 在OAuth2AutoConfiguration自动配置类中, 由内部的静态内部配置类SessionScopedConfiguration或RequestScopedConfiguration
//    都使用@Import注解引入了OAuth2ProtectedResourceDetailsConfiguration配置类, 该配置类定义了AuthorizationCodeResourceDetails作为实现, 并且是primary的bean, 
//    并且会读取security.oauth2.client下的配置属性, 这些属性都定义在BaseOAuth2ProtectedResourceDetails中, 包括:
//    id、grantType、clientId、accessTokenUri、scope、clientSecret、clientAuthenticationScheme、authorizationScheme、tokenName, 其中grantType写死为authorization_code 
// 2. 在OAuth2AutoConfiguration自动配置类中, 由内部的静态内部配置类SingletonScopedConfiguration中也会引入ClientCredentialsResourceDetails作为实现, 
//    但需要满足SingletonScopedConfiguration的生效条件
// 3. 这个属性其实就是就是当前的这个服务的一个客户端标识
final OAuth2ProtectedResourceDetails resource;

// 1. @EnableOAuth2Sso注解会引入了@EnableOAuth2Client注解, 而@EnableOAuth2Client注解引入了OAuth2ClientConfiguration配置类, 
//    该OAuth2ClientConfiguration配置类中定义了session的scope的OAuth2ClientContext这个bean
// 2. 在OAuth2AutoConfiguration自动配置类中, 由内部的静态内部配置类RequestScopedConfiguration或SingletonScopedConfiguration配置类中都会
//    使用@Bean来定义DefaultOAuth2ClientContext, 都需要满足这2个配置类上的生效条件才会生效。
// 3. 在@EnableOAuth2Sso注解使用@Import注解引入的ResourceServerTokenServicesConfiguration
//    或是由OAuth2AutoConfiguration自动配置类上使用@Import注解引入OAuth2ResourceServerConfiguration,
//    在OAuth2ResourceServerConfiguration配置类中所定义的UserInfoRestTemplateFactory, 会自动注入容器中的OAuth2ClientContext这个bean, 
//    在DefaultUserInfoRestTemplateFactory的getUserInfoRestTemplate()方法中, 就会DefaultUserInfoRestTemplateFactory的createOAuth2RestTemplate(..)方法,
//    当容器中已经定义了OAuth2ClientContext这个bean的话, 就是用容器中的, 否则, 就在OAuth2RestTemplate的构造器中创建1个默认的DefaultOAuth2ClientContext
// 4. 这个属性要记住关系: 1个OAuth2RestTemplate对应1个OAuth2ClientContext, 但是注意它是个代理对象, 并且被ScopedProxyMode.INTERFACES方式给增强了
OAuth2ClientContext context;

// 当获取令牌发生错误, 并且曾经获取过令牌时, 是否要再次尝试获取令牌
boolean retryBadAccessTokens;

// 默认实现为: DefaultOAuth2RequestAuthenticator, 就是在发起获取用户信息的请求前, 添加Authorization请求头, 值为: Bearer + 空格 + 访问令牌
OAuth2RequestAuthenticator authenticator;

// 默认由AccessTokenProviderChain来维护1个列表, 
// 列表中包括: AuthorizationCodeAccessTokenProvider、ImplicitAccessTokenProvider、ResourceOwnerPasswordAccessTokenProvider、ClientCredentialsAccessTokenProvider
// 每个provider都代表1种授权模式
AccessTokenProvider accessTokenProvider; 
方法
getAccessToken()
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
    
   // 从OAuth2ClientContext属性中获取访问令牌
   OAuth2AccessToken accessToken = context.getAccessToken();

   // 如果访问令牌是空的, 或者访问令牌已过期, 那么去重新获取访问令牌
   if (accessToken == null || accessToken.isExpired()) {
      try {
          
         // 传入OAuth2ClientContext, 调用acquireAccessToken(OAuth2ClientContext)获取令牌
         accessToken = acquireAccessToken(context);
         
      }
      catch (UserRedirectRequiredException e) {
          
         // 发生UserRedirectRequiredException,
         
         // 将从OAuth2ClientContext属性中的访问令牌置为null
         context.setAccessToken(null);
         
         accessToken = null;
         
         // 获取UserRedirectRequiredException异常中的stateKey属性
         String stateKey = e.getStateKey();
         
         if (stateKey != null) {
            // e的stateKey作为key, e的e.stateToPreserve作为value 设置到 DefaultOAuth2ClientContext的 state属性这个Map中
            Object stateToPreserve = e.getStateToPreserve();
            
            if (stateToPreserve == null) {
               // 如果异常中未指定stateToPreserve属性, 则默认值取NONE
               stateToPreserve = "NONE";
            }
            
            // 1. 也就是当调用acquireAccessToken(OAuth2ClientContext)获取令牌发生UserRedirectRequiredException异常时, 
            //    会将UserRedirectRequiredException异常中的stateKey和stateToPreserve属性都存到DefaultOAuth2ClientContext的state属性这个Map中,
            // 2. 其中stateKey是生成的6位随机字符串, 比如: EamgVe, stateToPreserve是当前服务的登录地址, 比如: http://localhost:1111/login 
            //    会设置到 DefaultOAuth2ClientContext的state属性这个Map中去
            context.setPreservedState(stateKey, stateToPreserve);
         }
         
         // 将UserRedirectRequiredException异常抛出去, 被OAuth2ContextFilter处理
         throw e;
      }
   }
   
   return accessToken;
}
acquireAccessToken(OAuth2ClientContext)
protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context) {
   
   // 获取DefaultOauth2Context的accessTokenRequest属性
   AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
   
   // 如果DefaultOauth2Context的accessTokenRequest属性是空的, 则抛出AccessTokenRequiredException类型的异常, 
   // 所以创建DefaultOauth2Context时, 肯定要保证accessTokenRequest属性不为空
   if (accessTokenRequest == null) {
      throw new AccessTokenRequiredException("No OAuth 2 security context has been established. Unable to access resource '"+ this.resource.getId() + "'.", resource);
   }

   // 获取OAuth2ClientContext的accessTokenRequest属性, 再从accessTokenRequest属性这个Map中获取key为"state"对应的值
   // 那么这里肯定会有个疑问, 这个DefaultAccessTokenRequest的parameters属性怎么会有state对应的值呢? 
   // 这个就是在OAuth2ClientConfiguration中定义的accessTokenRequest()方法中, 使用@Value("#{request.parameterMap}得到请求参数设置给accessTokenRequest的
   String stateKey = accessTokenRequest.getStateKey();
   
   // 如果stateKey不为空, 也就是说, 现在用户在认证中心授权了, 然后携带着state和code过来了, 此时state不为空
   if (stateKey != null) {
      // 1. 从OAuth2ClientContext的state属性这个Map中获取 传过来的 state 对应的值(这个值例如: http://localhost:1111/login) 
      //    设置到 DefaultOauth2Context的accessTokenRequest属性的state属性中
      // 2. 那么OAuth2ClientContext的state属性这个Map为什么会有stateKey对应的值呢? 
      //    在用户由于在当前服务未认证, 被重定向到http://localhost:1111/login时, 由于抛出UserRedirectRequiredException异常, 
      //    在OAuth2RestTemplate的getAccessToken()方法中捕捉到该异常时, 就将stateKey和对应的值设置到了OAuth2ClientContext中了, 
      //    所以, 这里能从OAuth2ClientContext移除stateKey
      accessTokenRequest.setPreservedState(oauth2Context.removePreservedState(stateKey));
   }

   // 如果DefaultOAuth2ClientContext的accessToken属性不为空, 那么把这个accessToken设置到DefaultAccessTokenRequest的existingToken属性中
   OAuth2AccessToken existingToken = oauth2Context.getAccessToken();
   if (existingToken != null) {
      accessTokenRequest.setExistingToken(existingToken);
   }

   OAuth2AccessToken accessToken = null;
   
   // 交给accessTokenProvider获取令牌, accessTokenProvider的实现是个AccessTokenProviderChain链
   accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
   
   // 获取令牌为空, 抛出异常
   if (accessToken == null || accessToken.getValue() == null) {
      throw new IllegalStateException("Access token provider returned a null access token, which is illegal according to the contract.");
   }
   
   // 将得到的令牌设置到OAuth2ClientContext的accessToken属性中, 并且会将此令牌设置到OAuth2ClientContext的accessTokenRequest属性的existingToken中
   oauth2Context.setAccessToken(accessToken);
   
   return accessToken;
}

AccessTokenProviderChain

内部维护了AccessTokenProvider1个列表,和1个ClientTokenServices

public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails resource, AccessTokenRequest request){

   OAuth2AccessToken accessToken = null;
   OAuth2AccessToken existingToken = null;
   
   Authentication auth = SecurityContextHolder.getContext().getAuthentication();

   // 如果访问当前请求的用户是匿名用户, 并且resource的实现不是ClientCredentialsResourceDetails, 那么抛出InsufficientAuthenticationException异常
   // 因为除了客户端模式, 其它模式都是需要用户信息的
   if (auth instanceof AnonymousAuthenticationToken) {
      if (!resource.isClientOnly()) {
         throw new InsufficientAuthenticationException("Authentication is required to obtain an access token (anonymous not allowed)");
      }
   }

   // 如果是客户端模式 或者 用户已认证
   if (resource.isClientOnly() || (auth != null && auth.isAuthenticated())) {
       
      // 获取到当前AccessTokenRequest的existingToken属性
      existingToken = request.getExistingToken();
      
      // 如果当前AccessTokenRequest的existingToken属性的令牌为空 并且 clientTokenServices不为空, 那么 使用 clientTokenServices 查询一下当前用户的令牌
      if (existingToken == null && clientTokenServices != null) {
         existingToken = clientTokenServices.getAccessToken(resource, auth);
      }

      // 如果令牌不为空
      if (existingToken != null) {
         // 如果令牌过期了
         if (existingToken.isExpired()) {
            if (clientTokenServices != null) {
               // 移除该令牌
               clientTokenServices.removeAccessToken(resource, auth);
            }
            // 获取到令牌的刷新令牌
            OAuth2RefreshToken refreshToken = existingToken.getRefreshToken();
            // 使用刷新令牌得到访问令牌, 刷新令牌的操作在AccessTokenProviderChain的refreshAccessToken(..)方法中
            if (refreshToken != null && !resource.isClientOnly()) {
               accessToken = refreshAccessToken(resource, refreshToken, request);
            }
         }
         else {
            accessToken = existingToken;
         }
      }
   }

   // 如果用户未认证
   if (accessToken == null) {
       
      // 为该用户获取令牌(这个是重点)
      // 这里会遍历AccessTokenProviderChain链中的所有AccessTokenProvider列表, 找到支持该resource的AccessTokenProvider
      // 下面就以AuthorizationCodeAccessTokenProvider授权码流程为例, 分析获取令牌的过程
      accessToken = obtainNewAccessTokenInternal(resource, request);

      if (accessToken == null) {
         throw new IllegalStateException("An OAuth 2 access token must be obtained or an exception thrown.");
      }
   }

   // 使用clientTokenServices保存令牌和用户关系
   if (clientTokenServices != null && (resource.isClientOnly() || auth != null && auth.isAuthenticated())) {
      clientTokenServices.saveAccessToken(resource, auth, accessToken);
   }

   return accessToken;
}
AuthorizationCodeAccessTokenProvider
StateKeyGenerator stateKeyGenerator;           // 内部使用RandomValueStringGenerator生成随机的6位字符串
 
String scopePrefix;                            // 默认为: scope.

RequestEnhancer authorizationRequestEnhancer;  // 默认实现为: DefaultRequestEnhancer, 给授权请求的form参数添加参数的

boolean stateMandatory;                        // state是不是强制需要的, 如果由认证中兴重定向过来没有提供state, 那么会认为是CSRF攻击
obtainAccessToken(OAuth2ProtectedResourceDetails,rquest)

获取访问令牌,其实就是当客户端提供了授权码code和state时,使用授权码向授权服务器获取令牌;当未携带code和state时,则抛出UserRedirectRequiredException,并生成state,然后交给OAuth2ClientContextFilter处理

public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) {

   AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
   
   // 获取DefaultAccessTokenRequest的parameters属性中code对应的值, 即授权码 为空
   if (request.getAuthorizationCode() == null) {
       
      // 如果DefaultAccessTokenRequest的parameters属性中state也为空
      if (request.getStateKey() == null) {
          
         // 抛出UserRedirectRequiredException异常, 并组装该异常中的数据
         throw getRedirectForAuthorization(resource, request);
      }
      
      // 走获取授权码的逻辑, 这里完全是后台发起获取授权码请求的逻辑, 
      // 可以学习下RestTemplate的的使用方式, 如何携带cookie, 如何从响应头中获取Set-Cookie, 获取响应头中的Location头
      obtainAuthorizationCode(resource, request);
   }
   
   // 使用授权码获取令牌
   return retrieveToken(request, resource, getParametersForTokenRequest(resource, request), getHeadersForTokenRequest(request));

}
getRedirectForAuthorization(resource, request)

组装向授权服务发起获取授权码请求的参数,将这些参数封装到UserRedirectRequiredException异常中

private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource, AccessTokenRequest request) {

   TreeMap<String, String> requestParameters = new TreeMap<String, String>();
   
   // 携带请求参数: response_type=code、client_id=xxx、redirect_uri=yyy
   requestParameters.put("response_type", "code"); 
   requestParameters.put("client_id", resource.getClientId());
   String redirectUri = resource.getRedirectUri(request);
   if (redirectUri != null) {
      requestParameters.put("redirect_uri", redirectUri);
   }

   // 组装scope参数, 使用空格分隔
   if (resource.isScoped()) {

      StringBuilder builder = new StringBuilder();
      List<String> scope = resource.getScope();

      if (scope != null) {
         Iterator<String> scopeIt = scope.iterator();
         while (scopeIt.hasNext()) {
            builder.append(scopeIt.next());
            if (scopeIt.hasNext()) {
               builder.append(' ');
            }
         }
      }

      requestParameters.put("scope", builder.toString());
   }

   // 构建UserRedirectRequiredException异常, 传入: 
   UserRedirectRequiredException redirectException = new UserRedirectRequiredException(resource.getUserAuthorizationUri(), requestParameters);

   // 生成state对应的值, 如: EamgVe
   String stateKey = stateKeyGenerator.generateKey(resource);
   
   // 将生成的state对应的值保存到UserRedirectRequiredException异常的stateKey属性中, 值比如: EamgVe
   redirectException.setStateKey(stateKey);
   
   // 设置到DefaultAccessTokenRequest的stateKey属性中, 值比如: EamgVe
   request.setStateKey(stateKey);
   
   // 保存到UserRedirectRequiredException异常的stateToPreserve属性中, 值为: http://localhost:1111/login
   redirectException.setStateToPreserve(redirectUri);
   
   // 保存到DefaultAccessTokenRequest的state属性中, 值为: http://localhost:1111/login
   request.setPreservedState(redirectUri);

   return redirectException;
}
getParametersForTokenRequest(resource, request)

组装向授权服务发起携带授权码获取访问令牌的流程所需要的参数

private MultiValueMap<String, String> getParametersForTokenRequest(AuthorizationCodeResourceDetails resource, AccessTokenRequest request) {

   // 组装表单参数
   MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
   
   // 授权类型为: authorization_code
   form.set("grant_type", "authorization_code");
   // 携带授权码
   form.set("code", request.getAuthorizationCode());

   // 获取AccessTokenRequest的state属性, 这在OAuth2RestTemplate的acquireAccessToken(OAuth2ClientContext)方法中设置的
   // 因为 在将用户重定向到认证中心登录之前, 有生成1个state, 这个state在OAuth2RestTemplate的getAccessToken()方法中处理异常时, 保存在DefaultOAuthClientContext,
   // 然后, 当用户在认证中心登录后, 携带state和code重定向到我们服务, 所以就在OAuth2RestTemplate的acquireAccessToken(OAuth2ClientContext)方法中, 
   // 从DefaultOAuth2ClientContext中根据state参数移除, 并设置到DefaultAccessTokenRequest的state属性中, 因此这里可以从DefaultAccessTokenRequest中拿到state
   // 如果能拿到那么说明是调用当前携带授权码的请求是由之前重定向到认证中心的请求而触发的, 
   // 因为这个state参数是我们服务生成, 然后携带给认证中心的, 现在认证中心得把它传回来, 来确定当前请求不是被其它用户主动随便填的state
   Object preservedState = request.getPreservedState();
   if (request.getStateKey() != null || stateMandatory) {
      if (preservedState == null) {
         throw new InvalidRequestException("Possible CSRF detected - state parameter was required but no state could be found");
      }
   }

   // 获取并设置redirect_uri参数的逻辑
   String redirectUri = null;
   if (preservedState instanceof String) {
      redirectUri = String.valueOf(preservedState);
   }
   else {
      redirectUri = resource.getRedirectUri(request);
   }
   if (redirectUri != null && !"NONE".equals(redirectUri)) {
      form.set("redirect_uri", redirectUri);
   }

   return form;

}
retrieveToken
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) {

   try {
      // 默认实现为: DefaultClientAuthenticationHandler, 根据clientAuthenticationScheme选择认证方式, 
      // 比如请求头认证: Authorization - Basic + 空格 + Base64(clientId:clientSecret)
      authenticationHandler.authenticateTokenRequest(resource, form, headers);

      // 默认实现设置accept请求头为: application/json
      tokenRequestEnhancer.enhance(request, resource, form, headers);
      
      final AccessTokenRequest copy = request;

      // 留意下: RestTemplate的无参构造方法中添加了很多的HttpMessageConverter消息转换器
      final ResponseExtractor<OAuth2AccessToken> delegate = getResponseExtractor();
      
      ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() {
         @Override
         public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
            // 获取Set-Cookie响应头
            if (response.getHeaders().containsKey("Set-Cookie")) {
               copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
            }
            return delegate.extractData(response);
         }
      };
      return getRestTemplate().execute(getAccessTokenUri(resource, form),  // 获取security.oauth2.client.access-token-uri配置的值, 例如: http://localhost:1110/oauth/token
                                       getHttpMethod(), 
                                       getRequestCallback(resource, form, headers), 
                                       extractor , 
                                       form.toSingleValueMap()
                               );

   }
   catch (OAuth2Exception oe) {
      throw new OAuth2AccessDeniedException("Access token denied.", resource, oe);
   }
   catch (RestClientException rce) {
      throw new OAuth2AccessDeniedException("Error requesting access token.", resource, rce);
   }

}
UserRedirectRequiredException

它继承自RuntimeException异常

// 重定向地址
final String redirectUri;

// 请求参数
final Map<String, String> requestParams;

// stateKey
String stateKey;

// stateKey对应的数据
Object stateToPreserve;
DefaultOAuth2ClientContext

它实现了OAuth2ClientContext接口

// 访问令牌
OAuth2AccessToken accessToken;

// 访问令牌请求对象
AccessTokenRequest accessTokenRequest;

// 包含stateKey -> stateToPreserve
Map<String, Object> state = new HashMap<String, Object>();
AccessTokenRequest

它实现了AccessTokenRequest接口

final MultiValueMap<String, String> parameters;

Object state;

OAuth2AccessToken existingToken;

String currentUri;

String cookie;

Map<? extends String, ? extends List<String>> headers;
UserInfoTokenServices
loadAuthentication(accessToken)
@Override
public OAuth2Authentication loadAuthentication(String accessToken) {

    // userInfoEndpointUrl由security.oauth2.resource.user-info-uri来配置, 
    // 在ResourceServerTokenServicesConfiguration的UserInfoTokenServicesConfiguration配置类中就会获取ResourceServerProperties的userInfoUri属性
    Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
    
    if (map.containsKey("error")) {
        throw new InvalidTokenException(accessToken);
    }
    
    // 读取响应封装为OAuth2Authenticaiton认证对象
    return extractAuthentication(map);
}
extractAuthentication(Map<String, Object>)
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
    
   // 交给principalExtractor属性, 提取为principal, 
   // 默认实现为: FixedPrincipalExtractor, 它会按顺序提取: "user", "username","userid", "user_id", "login", "id", "name"作为principal
   Object principal = getPrincipal(map);
   
   // 交给authoritiesExtractor属性, 提取为权限
   // 默认实现为: FixedAuthoritiesExtractor, 它会提取map中的"authorities"的key对应的值
   List<GrantedAuthority> authorities = this.authoritiesExtractor.extractAuthorities(map);
   
   // 封装 OAuth2Request, 仅指定了clientId和approved
   OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,null, null, null, null);
   
   // 封装为UsernamePasswordAuthenticationToken认证对象
   UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
   
   token.setDetails(map);
   return new OAuth2Authentication(request, token);
}

SSO服务端

同授权服务

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security OAuth2可以实现单点登录功能。通过引入相关的jar包,可以在Spring Boot项目中使用注解来实现单点登录客户端的功能。具体步骤如下: 1. 创建一个单点登录客户端工程,并引入以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </dependency> ``` 2. 在项目中使用注解来配置单点登录客户端的相关信息,例如授权服务器的URL、客户端ID和密钥等。 需要注意的是,根据Spring Security官方的最新推荐,Spring Security OAuth2项目已经不再推荐使用,而是将OAuth2的相关功能抽取出来,集成在Spring Security中,并单独新建了spring-authorization-server项目来实现授权服务器的功能。因此,如果需要实现授权服务器的功能,可以使用spring-authorization-server项目。 总结来说,Spring Security OAuth2可以实现单点登录功能,但是根据最新的推荐,建议使用Spring Security和spring-authorization-server来实现授权服务器的功能。\[1\]\[2\] #### 引用[.reference_title] - *1* *2* [SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统](https://blog.csdn.net/wmz1932/article/details/124719588)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Security OAuth2 单点登录](https://blog.csdn.net/weixin_42073629/article/details/115436378)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值