备忘录 - Spring Security 4.x

引用资料

Spring Security 项目

博客 Spring Security 认证架构

博客 Spring Security 专栏

说明

本文适用于对 Spring Security 进行源码分析,里面都是按照我自己源码分析流程写的,是顺着源码分析思路阐述的,所以不会有很多结论性的文字(要看总结性文字的可以参考其他博客,这里需要自己去分析总结), 同时也为之后查看源码排查错误指明方向.

这里会按照源码分析的过程逐步介绍 Spring Security 核心类 , 以后如果忘记了,按照这个顺序查看源码记忆起来. IDEA 的快捷键用起来, Ctrl-H(查看实现类),Ctrl-Shift-B(查看实现方法),Ctrl+Alt+左方向键(右方向键)(返回,前进到上次的位置 ) 不至于迷失在源码的大海中.读源码的时候,用笔记下类之间的联系,画个图(右脑记忆联想).否则看着看着就犯困,迷失在代码里面,不知道自己在干啥了.

源码分析一定是在已经知道使用方法之后在进行的,知道了些许概念.之前可以从各种博客里面看了很多如何使用Spring Security 干啥啥啥的教程 , 但是对整个框架的接口一知半解,出现问题抓耳挠腮.这个时候你就需要进行源码阅读了,通过源码了解你写的代码构建了哪些不为人知的骚操作

核心组件类

从 SecurityContextHolder 开始

SecurityContextHolder 按照某种策略持有 SecurityContext , 这些策略是

  • ThreadLocalSecurityContextHolderStrategy: 保存在 ThreadLocal
  • InheritableThreadLocalSecurityContextHolderStrategy: 保存在 ThreadLocal ,但可以延续到子线程
  • GlobalSecurityContextHolderStrategy : 全局共享单个 SecurityContext 实例,通常用在富客户端中,例如 Swing程序

SecurityContext 意思就是安全上下文 , 它用来获取 Authentication 身份认证信息

Authentication 是个抽象接口表示 身份信息 , 用来保存 GrantedAuthority 集合 , Credentials 登录凭证 , Details 详细信息 , Principal 身份标识 ,以及判断是否已经认证 isAuthenticated()

GrantedAuthority 表示权限抽象 , 有个实现类是 SimpleGrantedAuthority 用来表示 ROLE_*** 的字符串角色权限

AbstractAuthenticationToken 实现了 Authentication , 用来表示访问的用户实体信息 , 之后自定义身份认证的Token信息都可以通过继承它来实现 .

扫描二维码关注公众号,回复: 117504 查看本文章

AbstractAuthenticationToken 做了那些事?

  • 构造一个不可变的 GrantedAuthority 集合,显然每次认证后的身份权限应该是不变的
  • 擦除敏感信息 eraseCredentials()
  • 获取身份标识字符串名称
  • equals 比较

Principal 是身份标识的抽象 , 默认为 UserDetail , 要扩展的话就实现 Principal .

下面看下 AbstractAuthenticationToken 有哪些实现类:

UsernamePasswordAuthenticationToken 用来实现 用户名/密码登录形式.

观察下 UsernamePasswordAuthenticationToken 的设计,它有两个构造函数,一个会设置 authenticated 为 false , 一个传入 权限信息后 设置 authenticated 为 true . 后面会观察到, 用户初始访问时会被过滤器构造一个未认证的 UsernamePasswordAuthenticationToken 实例, 认证后会产生一个包含了权限信息的 UsernamePasswordAuthenticationToken 实例 . 自定义的Token也需要按照这个逻辑来

AnonymousAuthenticationToken 用来表征匿名用户,没有凭证信息,可以有权限

Token实现类其他的就先不看了.

前面看源码时发现了 UserDetail 类 , 它是用来存储用户信息/权限的不安全的类,实际内容会被转存到 Authentication.

UserDetail 有个实现类 User , 实现了些构造身份信息和授权信息的辅助方法

在同一包下有个 UserDetailsService , 加载(检索)用户信息的核心接口,一个方法 loadUserByUsername 通过用户名称查询 UserDetails 实例

看下实现类 UserDetailsManager接口又加了几个增删改查的方法

再看下 JdbcDaoImpl , JdbcUserDetailsManager , 这两个类 还有 User 类, 应该就是Spring Security提供的开箱即用的 基于数据库的安全信息检索实现提供者了 .

继续观察还有 CachingUserDetailsService 和 UserCache , 这个应该用在不维护服务端信息的程序中使用的,例如 remote clients or web services 的情形下使用的,UserCache 需要自己实现.

InMemoryUserDetailsManager 是一个基于内存的用户信息检索实现,官文中的示例用的就是它

AuthenticationManager 是认证相关的核心接口,是所有认证的起点.具体怎么认证是由 AuthenticationProvider 实现的.

看下它的实现类 ProviderManager , 这个类 包含了 :

  • AuthenticationEventPublisher 认证成功/失败事件的发布器
  • AuthenticationProvider 集合
  • 一个 父 AuthenticationManager

看看它实现 AuthenticationManager.authenticate() 做了啥: 迭代 provider 集合 , 调用 provider的authenticate()如果有结果就返回,否则调用父 AuthenticationManager 的 authenticate()逻辑有结果就返回 . 有结果就是认证成功,会发布 认证成功的事件,否则发布认证失败的事件并抛出异常

总而言之: AuthenticationManager 的认证委托给了支持 认证 AuthenticationToken 的 AuthenticationProvider 一个或多个实例

看下它的一个实现 AbstractUserDetailsAuthenticationProvider , 这个实现是用来对 UsernamePasswordAuthenticationToken认证的 , 看 authenticate()方法,它将UserDetails的检索和身份凭证的校验这两个变化的部分延迟到了子类实现.

默认的实现是 DaoAuthenticationProvider , 这个类实现了身份凭证校验环节(可以扩展 PasswordEncoder ) , 将 UserDetails 的检索委托给了 UserDetailsService 实现,似乎和前面分析的连接起来了.

梳理下: 以 UsernamePasswordAuthenticationToken 为例 , AuthenticationManager 根据 AuthenticationToken 类型调用 AuthenticationProvider 去执行用户身份认证. AuthenticationProvider 又将用户信息检索委托给了 UserDetailsService . 使用者只需要实现 UserDetailsService 去检索用户信息和权限 就可以集成 Spring Security 提供的开箱即用的 基于用户名和密码的认证体系了 .

看了这么多,整体的流程到底是怎样的?

好了,前面看源码画的图应该有很多箭头连着了,回顾梳理下流程,进入下一节.

Spring Security 的核心过滤器类

过滤器的源码分析,建议使用断点来干,首先写个demo,然后在某个过滤器的 doFilter方法中打个断点. 验证自己的想法.

前辈们都研究过了,我们根据他们的研究成果,验证一遍是不是这么回事就可以了. 和 1+1=2 的证明一样 , 大家都知道1+1=2了,论证下是不是等于2.

大致流程,以表单登录为例: 过滤器从session中取出身份信息(没登录过就是匿名),安全拦截器判定这个身份能否访问资源,如果能就过了.如果不能,判断下这个身份是否为匿名的,如果是认证过的身份,抛出异常返回个权限不足,403 什么的.如果是匿名用户就执行访问被拒绝的流程,就是重定向到登录页面,提交登录信息后,进行身份认证流程.这个时候你的身份就是认证过的了,并且存入了session,然后重定向到当初要访问的资源页面.然后再经过安全拦截器的判定.

看下 Spring-boot 的日志 o.s.s.web.DefaultSecurityFilterChain 的输出可以找到加载了安全系列的那些过滤器:

Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1
[
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@462e1e64
org.springframework.security.web.context.SecurityContextPersistenceFilter@593a6726
org.springframework.security.web.header.HeaderWriterFilter@14f40030
org.springframework.security.web.csrf.CsrfFilter@1cdc1bbc
org.springframework.security.web.authentication.logout.LogoutFilter@36327cec
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@2121d1f9
inaction.ss.security.ipLogin.IpAuthenticationProcessingFilter@69afa141
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@596a7f44
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@63a28987
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@49f40c00
org.springframework.security.web.session.SessionManagementFilter@4c447c09
org.springframework.security.web.access.ExceptionTranslationFilter@43c87306
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2ab26378
]

相关的分析详情参考下这篇博客 Spring Security(四)--核心过滤器源码分析

核心过滤器说明:

  • SecurityContextPersistenceFilter: 负责请求时加载SecurityContext 到 SecurityContextHolder , 结束请求时清理 SecurityContext.具体存储获取委托给 SecurityContextRepository 处理,这里默认使用的是 HttpSessionSecurityContextRepository 从Session获取SecurityContext.如果自己的程序也要获取 SecurityContext , 不要直接操作 session , 而是使用 SecurityContextHolder .
  • UsernamePasswordAuthenticationFilter: 表单登录认证的过滤器.如果请求的是登录页面,这个过滤器会构建Token,调用 AuthenticationManager进行认证.认证成功后会将认证信息存储到 SecurityContextHolder . 具体认证流程回顾下上一节画的图.
  • AnonymousAuthenticationFilter : 注意它在过滤器链中的位置,它位于登录过滤器之后,也就是说全部认证过滤器都经过后还是没有身份的话,就授予一个匿名身份.
  • ExceptionTranslationFilter: 负责转化它之后的过滤器抛出的异常.主要是两类 AccessDeniedException访问异常和AuthenticationException认证异常 . 认证异常或者 匿名用户的访问异常 会直接执行 AuthenticationEntryPoint (跳转到登录页面). 如果是认证用户的访问异常,会调用 AccessDeniedHandler 处理. AccessDeniedHandler 有两种处理方式,一种是跳转到错误页面,一种是仿佛错误状态码.
  • FilterSecurityInterceptor: 负责授权,就是访问决策管理.会调用 AccessDecisionManager 进行判定能够访问还是抛出 AccessDeniedException

过滤器说完了,再看下这些过滤器是怎么被添加到过滤器链中的

我们自己实现的 SecurityConfig 继承自 WebSecurityConfigurerAdapter,这里会有一个注解@EnableWebSecurity

EnableWebSecurity 导入了 WebSecurityConfiguration , SpringWebMvcImportSelector , 注解 @EnableGlobalAuthentication 又导入了 AuthenticationConfiguration. 所以我们需要分析上面这三个类干了啥

@EnableGlobalAuthentication 和 @EnableGlobalMethodSecurity

主要是开启方法级别的注解安全配置,基于AOP代理 , 参考下官方文档 Method Security

SpringWebMvcImportSelector

用来判断是否需要加载 WebMvcSecurityConfiguration 配置,具体可以看类注释

WebSecurityConfiguration

定位到方法 springSecurityFilterChain() ,这个就是来设置安全过滤器链的 , 还有 setFilterChainProxySecurityConfigurer 这个是用来装载 安全配置 SecurityConfigurer.分析下 webSecurity.build() ,就是对 SecurityConfigurer 集合中的每一个 SecurityConfigurer调用一次 configure 操作

WebSecurity 继承了 AbstractConfiguredSecurityBuilder , 看下 doBuild方法

见: AbstractConfiguredSecurityBuilder

@Override
	protected final O doBuild() throws Exception {
		synchronized (configurers) {
			buildState = BuildState.INITIALIZING;

			beforeInit();
			init();

			buildState = BuildState.CONFIGURING;

			beforeConfigure();
			configure();

			buildState = BuildState.BUILDING;

			O result = performBuild();

			buildState = BuildState.BUILT;

			return result;
		}
	}

demo里面的方法 其实就是添加了一系列的 SecurityConfigurer

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/index").permitAll()
                .antMatchers("/login/**").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                //默认使用表单登录器
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll()
        ;
    }

先来看下 SecurityConfigurer 具体有哪些实现类:

输入图片说明

在 WebSecurityConfigurerAdapter 的 getHttp()方法就可以看到默认添加的配置器

挑个 ExceptionHandlingConfigurer 看下它是怎么添加过滤器的

@Override
	public void configure(H http) throws Exception {
		AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
		ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
				entryPoint, getRequestCache(http));
		if (accessDeniedHandler != null) {
			exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler);
		}
		exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
		http.addFilter(exceptionTranslationFilter); //在这里
	}

看到这里应该很清楚Spring Security是怎么一步步通过配置构建安全过滤链的了.

流程分析清楚了,以过滤器为核心去了解如何配置就很方便了.

小技巧:如何快速找到某个安全过滤器对应的配置? 以 SecurityContextPersistenceFilter 为例 , 在 IDEA 里面进入 SecurityContextPersistenceFilter.java 选中类名,然后 Ctrl+F7 (查找使用类) 如图: 输入图片说明 然后在选中 SecurityContextConfigurer 类 Ctrl+F7 , 就跳转到 配置方法里面了 , 那么要配置 SecurityContextRepository 的话就执行

...
.and()
            .securityContext().securityContextRepository(repo)

到这里,整体的安全配置过程就结束了. 身份认证之后是权限控制,下一结开始分析权限决策管理

AccessDecisionManager 访问决策管理

基于投票策略的授权模型

源码分析: 从 FilterSecurityInterceptor 开始

FilterSecurityInterceptor 实现了 AbstractSecurityInterceptor

AbstractSecurityInterceptor 做了什么? 核心操作就是beforeInvocation中调用 AccessDecisionManager.decide(Authentication)进行授权判定

AccessDecisionManager 访问决策管理器

AbstractAccessDecisionManager 继承 AccessDecisionManager , 看下它持有了什么东西:

  • AccessDecisionVoter 集合 访问决策投票者

AccessDecisionVoter 定义了三种票证:

  • ACCESS_GRANTED 表示同意
  • ACCESS_DENIED 表示拒绝
  • ACCESS_ABSTAIN 表示弃权

下段文字引用自 http://elim.iteye.com/blog/2247057

Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。

       AffirmativeBased的逻辑是这样的:

       (1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

       (2)如果全部弃权也表示通过;

       (3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

       ConsensusBased的逻辑是这样的:

       (1)如果赞成票多于反对票则表示通过。

       (2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。

       (3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true, 
         则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

       (4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,
         如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

       UnanimousBased的逻辑与另外两种实现有点不一样,   
       另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,
       而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。
       这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,
       但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:

       (1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。

       (2)如果没有反对票,但是有赞成票,则表示通过。

       (3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。

Spring Security 默认使用 AffirmativeBased 决策

RoleVoter 对 以 ROLE_ 开头的 ConfigAttribute 进行判定 如果包含角色就通过,否则就拒绝.如果没有以 ROLE_ 开头的属性 则弃权

RoleHierarchyVoter 角色继承投票器 . 如果 角色A继承角色B , 那么角色A 就拥有角色B的权限. 通过 RoleHierarchy 来构建角色继承树 , 使用 A > B 表示 , A 继承 B

           <property name="hierarchy">
               <value>
                   ROLE_A > ROLE_B
                   ROLE_B > ROLE_AUTHENTICATED
                   ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED
               </value>
           </property>

AuthenticatedVoter 主要用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户。完全认证的用户是指由系统提供的登录入口进行成功登录认证的用户

AfterInvocationManager 调用后决策器,在 AccessDecisionManager 通过后进行再进行权限判定

AfterInvocationManager 委托 AfterInvocationProvider 进行判定

当开启 @EnableGlobalMethodSecurity(prePostEnabled = true) 时会进行 AfterInvocationManager 的注册

protected AfterInvocationManager afterInvocationManager() {
		if (prePostEnabled()) {
			AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
			ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
					getExpressionHandler());
			PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(
					postAdvice);
			List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<AfterInvocationProvider>();
			afterInvocationProviders.add(postInvocationAdviceProvider);
			invocationProviderManager.setProviders(afterInvocationProviders);
			return invocationProviderManager;
		}
		return null;
	}

结尾

引用一个非常重要的内容 WebSecurityConfigurerAdapter 配置的作用域 相关介绍

多 WebSecurityConfigurerAdapter 配置示例 和代码 https://gitee.com/zhwcong/inaction/tree/master/spring_secutiry

猜你喜欢

转载自my.oschina.net/congwei/blog/1807772