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

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

spring-security-oauth2-boot 2.2.0.RELEASE Single Sign On文档地址

spring-security-oauth2-boot-2.2.0.RELEASE-single_sign_on-01

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

  • @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的典型场景,很适合使用授权码流程


spring-security-oauth2-boot-2.2.0.RELEASE-single_sign_on-02

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

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

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

    注意:

    如果是在自定义的AutoConfiguration自动配置类上使用@EnableOAuth2Sso,在第一次重定向到授权服务器时会出现问题,具体是因为通过@EnableOAuth2Client添加的OAuth2ClientContextFilter会被放到springSecurityFilterChain这个Filter后面,导致无法拦截UserRedirectRequiredException需重定向异常

  • 如果没有自己的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登录后凭证,完成登录
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容