一、从Spring Security OAuth2官方文档了解@EnableOAuth2Sso作用

【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用-LMLPHP

先从第一段介绍开始,加上自己的分析:

  • @EnableOAuth2Sso是使用在OAuth2 Client角色上的注解,从其包路径也可以看出org.springframework.boot.autoconfigure.security.oauth2.client

  • @EnableOAuth2Sso单点登录的原理简单来说就是:标注有@EnableOAuth2Sso的OAuth2 Client应用在通过某种OAuth2授权流程获取访问令牌后(一般是授权码流程),通过访问令牌访问userDetails用户明细这个受保护资源服务,获取用户信息后,将用户信息转换为Spring Security上下文中的认证后凭证Authentication,从而完成标注有@EnableOAuth2Sso的OAuth2 Client应用自身的登录认证的过程。整个过程是基于OAuth2的SSO单点登录

  • SSO流程中需要访问的用户信息资源地址,可以通过security.oauth2.resource.userInfoUri配置指定
  • 最后的通过访问令牌访问受保护资源后,在当前服务创建认证后凭证Authentication(登录态)也可以不通过访问userInfoUri实现,userInfoUri端点是需要用户自己实现。默认情况security.oauth2.resource.preferTokenInfo=true ,获取用户信息使用的是授权服务器的/check_token端点,即TokenInfo,根据访问令牌找到在授权服务器关联的授予这个访问令牌的用户信息
  • Spring Security OAuth2 SSO整个流程实际上是 OAuth2 Client是一个运行在Server上的Webapp的典型场景,很适合使用授权码流程


【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用-LMLPHP

第二段主要讲了下如何使用@EnableOAuth2Sso

  • 使用@EnableOAuth2Sso的OAuth2 Client应用可以使用/login端点用于触发基于OAuth2的SSO流程,这个入口地址也可以通过security.oauth2.sso.login-path来修改

  • 如果针对一些安全访问规则有自己的定制,说白了就是自己实现了Spring Security的WebSecurityConfigurerAdapter想自定义一些安全配置,但又想使用@EnableOAuth2Sso的特性,可以在自己的WebSecurityConfigurerAdapter上使用@EnableOAuth2Sso注解,注解会在你的安全配置基础上做“增强”,至于具体如何“增强”的,后面的源码分析部分会详细解释

  • 如果没有自己的WebSecurityConfigurerAdapter安全配置,也可以在任意配置类上使用@EnableOAuth2Sso,除了添加OAuth2 SSO的增强外,还会有默认的基本安全配置


二、源码分析@EnableOAuth2Sso作用

首先来看一下@EnableOAuth2Sso的源码

/**
 * Enable OAuth2 Single Sign On (SSO). If there is an existing
 * {@link WebSecurityConfigurerAdapter} provided by the user and annotated with
 * {@code @EnableOAuth2Sso}, it is enhanced by adding an authentication filter and an
 * authentication entry point. If the user only has {@code @EnableOAuth2Sso} but not on a
 * WebSecurityConfigurerAdapter then one is added with all paths secured.
 *
 * @author Dave Syer
 * @since 1.3.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,
        ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {

}

可以看到主要做了几件事

  • 添加@EnableOAuth2Client
  • 启用OAuth2 SSO相关的OAuth2SsoProperties配置文件
  • 导入了3个配置类:OAuth2SsoDefaultConfigurationOAuth2SsoCustomConfigurationResourceServerTokenServicesConfiguration


@EnableOAuth2Client

@EnableOAuth2Client从名称就可以看出是专门给OAuth2 Client角色使用的注解,其可以独立使用,具体功能需要单独写一篇来分析,大致看一下源码,主要是导入了OAuth2ClientConfiguration配置类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OAuth2ClientConfiguration.class)
public @interface EnableOAuth2Client {

}

OAuth2ClientConfiguration配置类主要做了三件事

  • 向Servlet容器添加OAuth2ClientContextFilter
  • 创建request scope的Spring Bean: AccessTokenRequest
  • 创建session scope的Spring Bean: OAuth2ClientContext,OAuth2 Client上下文

大体上就是为OAuth2 Client角色创建相关环境


OAuth2SsoCustomConfiguration:OAuth2 SSO自定义配置

/**
 * Configuration for OAuth2 Single Sign On (SSO) when there is an existing
 * {@link WebSecurityConfigurerAdapter} provided by the user and annotated with
 * {@code @EnableOAuth2Sso}. The user-provided configuration is enhanced by adding an
 * authentication filter and an authentication entry point.
 *
 * @author Dave Syer
 */
@Configuration
@Conditional(EnableOAuth2SsoCondition.class)  //OAuth2 SSO自定义配置生效条件
public class OAuth2SsoCustomConfiguration
        implements ImportAware, BeanPostProcessor, ApplicationContextAware {

    private Class<?> configType;

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        this.configType = ClassUtils.resolveClassName(importMetadata.getClassName(),
                null);

    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        return bean;
    }

    /**
     * BeanPostProcessor的初始化后方法
     * 给用户自定义的WebSecurityConfigurerAdapter添加Advice来增强:SsoSecurityAdapter
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        // 如果是WebSecurityConfigurerAdapter,并且就是添加@EnableOAuth2Sso的那个
        if (this.configType.isAssignableFrom(bean.getClass())
                && bean instanceof WebSecurityConfigurerAdapter) {
            ProxyFactory factory = new ProxyFactory();
            factory.setTarget(bean);
            factory.addAdvice(new SsoSecurityAdapter(this.applicationContext));
            bean = factory.getProxy();
        }
        return bean;
    }

    /**
     * 拦截用户的WebSecurityConfigurerAdapter
     * 在其init()初始化之前,添加SsoSecurityConfigurer配置
     */
    private static class SsoSecurityAdapter implements MethodInterceptor {

        private SsoSecurityConfigurer configurer;

        SsoSecurityAdapter(ApplicationContext applicationContext) {
            this.configurer = new SsoSecurityConfigurer(applicationContext);
        }

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            if (invocation.getMethod().getName().equals("init")) {
                Method method = ReflectionUtils
                        .findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
                ReflectionUtils.makeAccessible(method);
                HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method,
                        invocation.getThis());
                this.configurer.configure(http);
            }
            return invocation.proceed();
        }
    }
}

OAuth2SsoCustomConfiguration自定义配置指的是如果用户有自定义的WebSecurityConfigurerAdapter安全配置的情况下,就在用户自定义配置的基础上做OAuth2 SSO的增强,具体分析为

  • 首先必须在满足@Conditional(EnableOAuth2SsoCondition.class)的情况下才可以使用,EnableOAuth2SsoCondition条件指的是@EnableOAuth2Sso注解被使用在WebSecurityConfigurerAdapter
  • 可以看到OAuth2SsoCustomConfiguration配置类也是一个BeanPostProcessor,其会在Spring初始化Bean的前后做处理,上面代码中会在Sping初始化WebSecurityConfigurerAdapter之后,并且就是添加了@EnableOAuth2Sso注解的WebSecurityConfigurerAdapter之后,为安全配置类做“增强”,添加了一个Advice为SsoSecurityAdapter
  • SsoSecurityAdapter会在用户添加了@EnableOAuth2Sso注解的WebSecurityConfigurerAdapter配置类调用init()初始化方法之前,先添加一段子配置SsoSecurityConfigurer,这个子配置就是实现基于OAuth2 SSO的关键


SsoSecurityConfigurer:OAuth2 SSO核心配置(增强)

class SsoSecurityConfigurer {

    public void configure(HttpSecurity http) throws Exception {
        OAuth2SsoProperties sso = this.applicationContext
                .getBean(OAuth2SsoProperties.class);
        // Delay the processing of the filter until we know the
        // SessionAuthenticationStrategy is available:
        http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso)));
        addAuthenticationEntryPoint(http, sso);
    }
    
  • 添加OAuth2ClientAuthenticationConfigurer子配置,为了向springSecurityFilterChain过滤器链添加一个专门用于处理OAuth2 SSO的OAuth2ClientAuthenticationProcessingFilter
  • 添加处理页面及Ajax请求未认证时的AuthenticationEntryPoint认证入口

OAuth2ClientAuthenticationConfigurer子配置是重点

// 创建OAuth2ClientAuthenticationProcessingFilter
private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(
        OAuth2SsoProperties sso) {
    OAuth2RestOperations restTemplate = this.applicationContext
            .getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
    ResourceServerTokenServices tokenServices = this.applicationContext
            .getBean(ResourceServerTokenServices.class);
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
            sso.getLoginPath());
    filter.setRestTemplate(restTemplate);
    filter.setTokenServices(tokenServices);
    filter.setApplicationEventPublisher(this.applicationContext);
    return filter;
}

// OAuth2ClientAuthenticationConfigurer子配置
private static class OAuth2ClientAuthenticationConfigurer
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private OAuth2ClientAuthenticationProcessingFilter filter;

    OAuth2ClientAuthenticationConfigurer(
            OAuth2ClientAuthenticationProcessingFilter filter) {
        this.filter = filter;
    }

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        OAuth2ClientAuthenticationProcessingFilter ssoFilter = this.filter;
        ssoFilter.setSessionAuthenticationStrategy(
                builder.getSharedObject(SessionAuthenticationStrategy.class));
        // 添加过滤器
        builder.addFilterAfter(ssoFilter,
                AbstractPreAuthenticatedProcessingFilter.class);
    }

}

OAuth2ClientAuthenticationConfigurer子配置将构造好的专门用于处理OAuth2 SSO场景的过滤器OAuth2ClientAuthenticationProcessingFilter添加到springSecurityFilterChain过滤器链中,构造这个Filter时需要

  • OAuth2RestOperations:专门用于和授权服务器、资源服务器做Rest交互的模板工具类
  • ResourceServerTokenServices:用于访问Token资源服务的类
  • SessionAuthenticationStrategy:OAuth2 SSO认证完成后,使用Spring Security的会话策略

这一步,向springSecurityFilterChain过滤器链中添加OAuth2ClientAuthenticationConfigurer是最核心的一步,整个OAuth2 SSO的交互都由这个Filter完成,OAuth2ClientAuthenticationConfigurer的具体逻辑待后续分析


OAuth2SsoDefaultConfiguration:OAuth2 SSO默认配置

/**
 * Configuration for OAuth2 Single Sign On (SSO). If the user only has
 * {@code @EnableOAuth2Sso} but not on a {@code WebSecurityConfigurerAdapter} then one is
 * added with all paths secured.
 *
 * @author Dave Syer
 * @since 1.3.0
 */
@Configuration
@Conditional(NeedsWebSecurityCondition.class)  //OAuth2Sso默认配置生效条件
public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter {

    private final ApplicationContext applicationContext;

    public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    /**
     * 1、添加/**都需要认证才能访问的限制
     * 2、添加SsoSecurityConfigurer配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();
        new SsoSecurityConfigurer(this.applicationContext).configure(http);
    }

    /**
     * OAuth2Sso默认配置生效条件
     */
    protected static class NeedsWebSecurityCondition extends EnableOAuth2SsoCondition {
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata));
        }
    }
}
  • 条件NeedsWebSecurityConditionEnableOAuth2SsoCondition相反,最后满足当用户使用了EnableOAuth2Sso,但其没有被放在自己定义的WebSecurityConfigurerAdapter安全配置类上时,会进入OAuth2 SSO默认配置,从注释信息也可以看出
  • OAuth2SsoDefaultConfiguration继承了WebSecurityConfigurerAdapter,是一段Spring Security的安全配置
  • 添加满足/**路径的请求都需要authenticated()认证,默认安全配置
  • 和上面分析一样,使用SsoSecurityConfigurer子配置,最终会为springSecurityFilterChain过滤器链中添加OAuth2ClientAuthenticationConfigurer


ResourceServerTokenServicesConfiguration:访问Token资源服务的配置

主要作用是创建ResourceServerTokenServices,用于通过访问令牌获取其相关的用户凭据,或者读取访问令牌的完整信息,接口定义如下

public interface ResourceServerTokenServices {
    /**
     * Load the credentials for the specified access token.
     * 加载指定访问令牌的凭据
     *
     * @param accessToken The access token value.
     * @return The authentication for the access token.
     * @throws AuthenticationException If the access token is expired
     * @throws InvalidTokenException if the token isn't valid
     */
    OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

    /**
     * Retrieve the full access token details from just the value.
     * 仅从值中检索完整的访问令牌详细信息
     *
     * @param accessToken the token value
     * @return the full access token with client id etc.
     */
    OAuth2AccessToken readAccessToken(String accessToken);
}

具体的ResourceServerTokenServices接口实现分为

  • RemoteTokenServices:远端的TokenService
    • TokenInfoServices:访问/check_token端点,根据访问令牌找到在授权服务器关联的授予这个访问令牌的用户信息
    • UserInfoTokenServices:访问用户自定义的userInfo端点,根据访问令牌访问受保护资源userInfo
  • JwtTokenServices:基于Json Web Token自包含令牌的TokenService

在通过以上ResourceServerTokenServices接口实现获取用户信息后,就可以在使用@EnableOAuth2Sso注解的OAuth2 Client上创建已认证的用户身份凭证Authentication,完成登录


三、总结

总的来说@EnableOAuth2Sso注解帮助我们快速的将我们的OAuth2 Client应用接入授权服务器完成基于OAuth2的SSO流程,创建登录状态

无论是用户有没有自己的WebSecurityConfigurerAdapter安全配置都可以使用@EnableOAuth2Sso注解,如果有,@EnableOAuth2Sso是在用户的安全配置上做增强

增强的逻辑是在SpringSecurityFilterChain过滤器链上添加OAuth2ClientAuthenticationProcessingFilter这个用于登录认证的Filter,其使用的是OAuth2授权码流程,以下都是这个Filter负责的功能

  • 将用户重定向到授权服务器获取授权
  • 根据code授权码和OAuth2 clientId、secret获取访问令牌
  • 最后使用ResourceServerTokenServices并携带访问令牌获取用户信息,创建Authentication登录后凭证,完成登录
12-09 11:57