2.Spring Security 认证授权

8. 【源码分析】Spring Security 认证授权

总揽

8.1 结构总揽

Spring Security 所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入
系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通
过 Filter 或 AOP 等技术来实现,SpringSecurity 对 Web 资源的保护是靠 Filter 实现的,所以
从这个 Filter 来入手,逐步深入 Spring Security 原理。当初始化 Spring Security 时,会创
建 一 个 名 为 SpringSecurityFilterChain 的 Servlet 过 滤 器 , 类 型 为
org.springframework.security.web.FilterChainProxy,它实现了 javax.servlet.Filter,
因此外部的请求会经过此类,下图是 Spring Security 过虑器链结构图:
在这里插入图片描述

8.2 上图说明
FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain
所包含的各个 Filter,同时这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核
心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给
了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理
下图是 FilterChainProxy 相关类的 UML 图示
在这里插入图片描述

spring Security 功能的实现主要是由一系列过滤器链相互配合完成
在这里插入图片描述

8.3 过滤器链中主要的几个过滤器及其作用

8.3.1SecurityContextPersistenceFilter
这个 Filter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开
始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给
SecurityContextHolder 。 在 请 求 完 成 后 将 SecurityContextHolder 持 有 的
SecurityContext 再 保 存 到 配 置 好 的 SecurityContextRepository , 同 时 清 除
securityContextHolder 所持有的 SecurityContext;
8.3.2UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或
失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这
些都可以根据需求做相关改变;
8.3.3FilterSecurityInterceptor
是用于保护 web 资源的,使用 AccessDecisionManager 对当前用户进行授权访问;
8.3.4ExceptionTranslationFilter
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:
AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。 9. 【源码分析】Spring Security 认证工作
流程
UsernamePasswordAuthenticationFilter (attemptAuthentication)
ProviderManager(authenticate)
DaoAuthenticationProvider (retrieveUser)
AbstractUserDetailsAuthenticationProvider(authenticate)

9.1 认证流程图

在这里插入图片描述

9.2 流程图分析

  1. 用 户 提 交 用 户 名 、 密 码 被 SecurityFilterChain 中 的
    UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求 Authentication,
    通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
  2. 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证
  3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提
    到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通
    过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。可
    以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证
    的出发点,它的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此
    ProviderManager 维护着一个 List 列表,存放多种认证方式,
    最终实际的认证工作是由 AuthenticationProvider 完成的。咱们知道 web 表单的对应的
    AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个
    UserDetailsService 负 责 UserDetails 的 获 取 。 最 终 AuthenticationProvider 将
    UserDetails 填充至 Authentication

9.3 断点调试及源码分析

看上图打断点调试

9.4 结果总结

9.4.1AuthenticationProvider
通过前面的 Spring Security 认证流程我们得知,认证管理器(AuthenticationManager)委托
AuthenticationProvider 完成认证工作。
AuthenticationProvider 是一个接口,定义如下:

public interface AuthenticationProvider {
/**
* 认证
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
/**
* 判断认证方式
*/
boolean supports(Class<?> authentication);
}

authenticate()方法定义了认证的实现过程,它的参数是一个 Authentication,里面包含
了登录用户所提交的用户、密码等。而返回值也是一个 Authentication,这个 Authentication
则是在认证成功后,将用户的权限及其他信息重新组装后生成。
Spring Security 中维护着一个 List 列表,存放多种认证方
式,不同的认证方式使用不同的 AuthenticationProvider。如使用用户名密码登录时,使用
AuthenticationProvider1,短信登录时使用 AuthenticationProvider2 等等这样的例子很多。
每个 AuthenticationProvider 需要实现 supports()方法来表明自己支持的认证方式,如
我 们 使 用 表 单 方 式 认 证 , 在 提 交 请 求 时 Spring Security 会 生 成
UsernamePasswordAuthenticationToken,它是一个 Authentication,里面封装着用户提交的
用户名、密码信息。而对应的,哪个 AuthenticationProvider 来处理它?
我们在 DaoAuthenticationProvider 的基类 AbstractUserDetailsAuthenticationProvider
发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

也就是说当 web 表单提交用户名密码时,Spring Security 由 DaoAuthenticationProvider 处
理。
最后,我们来看一下 Authentication(认证信息)的结构,它是一个接口,我们之前提到的
UsernamePasswordAuthenticationToken 就是它的实现之一:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();

Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

(1)Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal
是位于 java.security
包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个 getName()方法。
(2)getAuthorities(),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是
代表权限信息的一系
列字符串。
(3)getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保
障安全。
(4)getDetails(),细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,
它记录了访问者的 ip 地
址和 sessionId 的值。
(5)getPrincipal(),身份信息,大部分情况下返回的是 UserDetails 接口的实现类,
UserDetails 代表用户的详细
信息,那从 Authentication 中取出来的 UserDetails 就是当前登录用户信息,它也是框架中的
常用接口之一。

9.4.2UserDetailsService【重点】

9.4.2.1 认识 UserDetailsService
现在咱们现在知道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到
一个 Authentication(UsernamePasswordAuthenticationToken 实现),里面包含了身份信息
(Principal)。这个身份信息就是一个 Object ,大多数情况下它可以被强转为 UserDetails
对象。
DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取
用 户 信 息 UserDetails( 包 含 密 码 ) , 而 后 DaoAuthenticationProvider 会 去 对 比
UserDetailsService 提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此
可以通过将自定义的 UserDetailsService 公开为 spring bean 来定义自定义身份验证。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
很 多 人 把 DaoAuthenticationProvider 和 UserDetailsService 的 职 责 搞 混 淆 , 其 实
UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而
DaoAuthenticationProvider 的职责更大,它完成完整的认证流程,同时会把 UserDetails 填充
至 Authentication。
上面一直提到 UserDetails 是用户信息,咱们看一下它的真面目:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有 username,authorities。Authentication
的 getCredentials()与 UserDetails 中的 getPassword()需要被区分对待,前者是用户提交的
密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication 中的
getAuthorities() 实 际 是 由 UserDetails 的 getAuthorities() 传 递 而 形 成 的 。 还 记 得
Authentication 接口中的 getDetails()方法吗?其中的 UserDetails 用户详细信息便是经过了
AuthenticationProvider 认证之后被填充的。
通过实现 UserDetailsService 和 UserDetails,我们可以完成对用户信息获取方式以及用户信息
字段的扩展。
Spring Security 提 供 的 InMemoryUserDetailsManager( 内 存 认 证 ) ,
JdbcUserDetailsManager(jdbc 认证)就是
UserDetailsService 的实现类,主要区别无非就是从内存还是从数据库加载用户。
9.4.2.2 测试
自定义 UserDetailsService
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//登录账号
System.out.println(“username=”+username);
//根据账号去数据库查询…
//这里暂时使用静态数据
UserDetails userDetails =
User.withUsername(username).password(“123456”).authorities(“hello:query”).build();
return userDetails;
}
}

重启工程,请求认证,SpringDataUserDetailsService 的 loadUserByUsername 方法被调用 ,
查询用户信息。

10. 【源码分析】Spring Security 授权工作流程

10.1 授权流程图

通过快速上手我们知道,Spring Security 可以通过 http.authorizeRequests() 对 web 请进行授权保护。SpringSecurity 使用标准 Filter 建立了对 web 请求的拦截,最终实现对资源的
授权访问。
Spring Security 的授权流程如:
在这里插入图片描述

10.2 授权流程分析

10.2.1 拦截请求

已 认 证 用 户 访 问 受 保 护 的 web 资 源 将 被 SecurityFilterChain 中 的
FilterSecurityInterceptor 的子类拦截。
10.2.2 获取资源访问策略
FilterSecurityInterceptor 会 从 SecurityMetadataSource 的 子 类
DefaultFilterInvocationSecurityMetadataSource 获 取 要 访 问 当 前 资 源 所 需 要 的 权 限
Collection 。
SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访
问规则, 读取访问策略如
10.2.3 最后
FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通
过,则允许访问资源,否则将禁止访问。
AccessDecisionManager(访问决策管理器)的核心接口如下:
在这里插入图片描述

public interface AccessDecisionManager {
// ~ Methods
//
===========================================================================================
=============
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*
* @param authentication the caller invoking the method (not null)
* @param object the secured object being called
* @param configAttributes the configuration attributes associated with the secured
* object being invoked
*/
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
}

这里着重说明一下 decide 的参数:
authentication:要访问资源的访问者的身份
object:要访问的受保护资源,web 请求对应 FilterInvocation
configAttributes:是受保护资源的访问策略,通过 SecurityMetadataSource 获取。
decide 接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

10.3 授权决策分析

AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication
是否有权访问受保护对象进行投票,AccessDecisionManager 根据投票结果,做出最终决策。
AccessDecisionVoter 是一个接口,其中定义有三个方法,具体结构如下所示。
public interface AccessDecisionVoter {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vo****te(Authentication var1, S var2, Collection var3);

}
vote()方法的返回结果会是 AccessDecisionVoter 中定义的三个常量之一。ACCESS_GRANTED 表
示同意,ACCESS_DENIED 表示拒绝,ACCESS_ABSTAIN 表示弃权。如果一个 AccessDecisionVoter
不能判定当前 Authentication 是否拥有访问对应受保护对象的权限,则其 vote()方法的返回值
应当为弃权 ACCESS_ABSTAIN。
Spring Security 内置了三个基于投票的 AccessDecisionManager 实现类如下,它们分别
是 AffirmativeBased、ConsensusBased 和 UnanimousBased,。
10.3.1 AffirmativeBased 的逻辑是:
(1)只要有 AccessDecisionVoter 的投票为 ACCESS_GRANTED 则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出 AccessDeniedException。
Spring security 默认使用的是 AffirmativeBased。
10.3.2 ConsensusBased 的逻辑是:
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出 AccessDeniedException。
(3)如果赞成票与反对票相同且不等于 0,并且属性 allowIfEqualGrantedDeniedDecisions
的 值 为 true , 则 表 示 通 过 , 否 则 将 抛 出 异 常 AccessDeniedException 。 参 数
allowIfEqualGrantedDeniedDecisions 的值默认为 true。
(4)如果所有的 AccessDecisionVoter 都弃权了,则将视参数 allowIfAllAbstainDecisions
的值而定,如果该值为 true 则表示通过,否则将抛出异常 AccessDeniedException。参数
allowIfAllAbstainDecisions 的值默认为 false。
10.3.3 UnanimousBased 的逻辑具体是:
UnanimousBased 的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性
全 部 传 递 给 AccessDecisionVoter 进 行 投 票 , 而 UnanimousBased 会 一 次 只 传 递 一 个
ConfigAttribute 给 AccessDecisionVoter 进 行 投 票 。 这 也 就 意 味 着 如 果 我 们 的
AccessDecisionVoter 的逻辑是只要传递进来的
ConfigAttribute 中有一个能够匹配则投赞成票,但是放到 UnanimousBased 中其投票结果就不一
定是赞成了。
UnanimousBased 的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个 ConfigAttribute 被任意的 AccessDecisionVoter 反对了,
则将抛出 AccessDeniedException。
(2)如果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数 allowIfAllAbstainDecisions 的值而定,true 则通过,
false 则抛出 AccessDeniedException。
Spring Security 也 内 置 一 些 投 票 者 实 现 类 如 RoleVoter 、 AuthenticatedVoter 和
WebExpressionVoter 等,可以自行查阅资料进行学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值