前言
已经写了好几篇关于 Spring Security 的文章了,相信很多读者还是对 Spring Security 的云里雾里的。这是因为对 Spring Security 中的对象还不了解。本文就来介绍介绍一下常用对象。
认证流程
SecurityContextHolder
用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保存在会话中。Spring Security 提供会话管理,认证通过后将身份信息放入 SecurityContextHolder 上下文,SecurityContext 与当前线程进行绑定,方便获取用户身份。
// 获取当前登录的用户信息
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
复制代码
AuthenticationManager
认证管理器,AuthenticationManager 是认证相关的核心接口,是发起认证的入口,用于处理认证请求。接口只提供了一个认证方法,方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。最常见的实现是ProviderManager
。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
复制代码
ProviderManager
提供商管理器,ProviderManager
是 AuthenticationManager
的一个实现类,提供了基本的认证逻辑和方法。它其中包含了一个 List 的 AuthenticationProvider
的属性,该属性存放多种认证方式!为什么需要这个属性呢?当Spring Security
默认提供的认证方式不能满足需求时,就可以通过 AuthenticationProvider
接口来扩展出其他认证方式,比如邮箱+验证码,手机号码+验证码登录。
AuthenticationProvider
AuthenticationProvider
(身份验证提供者),可以将多个AuthenticationProvider
实例添加到ProviderManager
中。其每个AuthenticationProvider
可以执行特定的 Authentication
(身份验证)类型。例如:DaoAuthenticationProvider
支持基于用户名+密码的 UsernamePasswordAuthenticationToken
身份验证。也可以自定义认证方式,比如自定义EmailVerificationCodeAuthenticationProvider
支持邮箱 + 验证码的 EmailVerificationCodeAuthenticationToken
身份验证。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
复制代码
该接口中有两个方法,如下:
-
authenticate()
方法接收一个未通过认证Authentication
对象,返回一个通过认证的Authentication
对象。可以实现authenticate()
方法来自定义身份验证逻辑。 -
supports(Class<?> authentication)
方法接收一个Authentication(身份验证) 对象
,如果AuthenticationProvider
支持指定的身份验证对象,则返回 true。 但是返回 true 并不保证AuthenticationProvider
能够对提供的Authentization
类实例进行身份验证。它只是表明它可以支持对其进行更深入的验证。AuthenticationProvider
仍可以从authenticate()
方法返回 null,以尝试其他的AuthentitationProvider
进行验证。
Authentication
Authentication(身份验证)
接口是 Spring Security 中身份验证流程的顶级接口,该接口定义了如下方法:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
复制代码
方法含义如下:
方法 | 描述 |
---|---|
getAuthorities | 获取登录用户的权限列表 |
getCredentials | 获取凭据。用户密码登录,这个字段就是密码信息,在认证过后通常会被移除,用于保障安全。如果是手机号验证码登录,那这个字段存的就是验证码 |
getDetails | 包含了一些认证时的信息,默认的实现为 WebAuthenticationDetails,记录了访问者的远程地址和sessionId的值。 |
getPrincipal | 身份信息,默认情况下返回的是 UserDetails 的实例 |
isAuthenticated | 是否通过认证,通过认证为 true |
setAuthenticated | 设置是否已认证 |
getName | 用户名 |
具体响应内容可以参考如下:
{
"authorities": [
{
"authority": "ROLE_admin"
},
{
"authority": "ROLE_user"
}
],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "D77AF630A476DEE7A2A75B1D751C4CF1"
},
"authenticated": true,
"principal": {
"password": null,
"username": "cxyxj",
"authorities": [
{
"authority": "ROLE_admin"
},
{
"authority": "ROLE_user"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "cxyxj"
}
复制代码
Authentication
本身是一个接口,它有很多实现类:
在众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌)
,但是这个类就只有简单的50行左右的代码,其中有两个属性,principal 代表用户名,credentials 代表密码。还有两个构造方法,一个是代表未认证的,一个是代表已认证的;三个set、get方法,一个擦除凭据方法。该类继承了 AbstractAuthenticationToken
,其大部分逻辑在父类中,当然父类的逻辑也非常简单。 所以 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌)
的作用就是将用户输入的用户名和密码进行封装,并供给 AuthenticationManager
进行验证。
UserDetails
这个接口定义了用户的核心信息,比如用户名、密码、账号是否过期、是否锁定等!默认实现类org.springframework.security.core.userdetails.User。在 Spring Security 中,如果自定义认证逻辑时,需要实现该接口进行扩展,来保存自己系统的用户信息。接口定义如下方法:
方法 | 描述 |
---|---|
getAuthorities | 获取用户权限 |
getPassword | 获取用户密码 |
getUsername | 获取用户名 |
isAccountNonExpired | 账户是否未过期,true:未过期,false:过期 |
isAccountNonLocked | 账户是否未锁定,true:未锁定,false:锁定 |
isCredentialsNonExpired | 凭证(密码)是否未过期,true:未过期,false:过期 |
isEnabled | 账户是否启用,true:启用,false:禁用 |
一个正常能登录的账号,四个状态都是为 true 的。
UserDetailsService
在 Spring Security 中,什么也不进行配置时,账号和密码是由 Spring Security 自动生成的。 但在实际的项目中账号、密码是从数据库中查询出来的。所以我们需要自定义认证逻辑。 此时需要实现 UserDetailsService 接口。而 UserDetailsService 接口中只定义了一个方法,作用是根据用户名加载用户,获得 UserDetails 对象。
public interface UserDetailsService {
// 按用户名加载用户
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码
可以参考自定义逻辑如下:
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(SysUser::getAccount, account);
// 根据用户名查询用户
SysUser sysUsers = sysUserMapper.selectOne(queryWrapper);
if (Objects.isNull(sysUsers)) {
Assert.isTrue(true,"用户名或者密码错误");
}
// 获得用户角色信息
List<String> roles = sysUserMapper.selectByUserId(sysUsers.getUserId());
// 构建 SimpleGrantedAuthority 对象
List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new SysUserDetails(sysUsers, authorities);
}
复制代码
除了需要手动实现 UserDetailsService 接口的方式外,Spring Security 也内置了几种方式。 我们来看下 UserDetailsService 都有哪些实现类:
-
InMemoryUserDetailsManager:内存用户,这种方式在学习 Spring Security 的时候,用的非常多。
-
JdbcUserDetailsManager:通过 JDBC 的方式将数据库和 Spring Security 连接起来。它自己提供了一个数据库脚本,脚本路径如下:
org/springframework/security/core/userdetails/jdbc/users.ddl
。脚本的内容呢,应该是不符合项目实际开发的,所以这个也是在学习的时候可以使用使用。
PasswordEncoder
PasswordEncoder 接口用于执行密码的单向转换,以便安全地存储密码。
InteractiveAuthenticationSuccessEvent
身份验证成功后,发布一个名为InteractiveAuthenticationSuccessEvent
的事件通知给到应用上下文,用于告知身份验证已经成功。
FilterChainProxy
在 Spring Security 的默认配置中,将创建一个名为 springSecurityFilterChain 的 servlet 过滤器作为bean。默认情况下,Spring Security 内置了一个过滤链,链中有 15 个过滤器。
想要了解更多请前往: 深入理解 FilterChainProxy【源码篇】
ExceptionTranslationFilter
ExceptionTranslationFilter 异常转换过滤器位于整个 springSecurityFilterChain 的后方,用来转换整个链路中出现的异常。此过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,一般处理两大类异常:AccessDeniedException 已登录无权限访问异常和 AuthenticationException 未认证访问异常。
HeaderWriterFilter
用来给http响应添加一些Header,比如X-Frame-Options, X-XSS- Protection*,X-Content-Type-Options.
CsrfFilter
用于防止csrf攻击(跨站点请求伪造(Cross- site request forgery))。
LogoutFilter
处理注销的过滤器。
RequestCacheAwareFilter
内部维护了一个RequestCache,用于缓存request请求。
SecurityContextHolderAwareRequestFilter
对ServletRequest进行了一次包装,使得request 具有更加丰富的API。
SessionManagementFilter
和session相关的过滤器,内部维护了一个 SessionAuthenticationStrategy,两者组合使用,常用来防止会话固定攻击保护( session- fixation protection attack ),以及限制同一用户开启多个会话的数量。
AnonymousAuthenticationFilter
匿名身份过滤器,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
UsernamePasswordAuthenticationFilter
表单提交了username和password参数,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的。在表单认证的流程中,这是最最关键的过滤器。
AbstractAuthenticationProcessingFilter
翻译为:抽象身份验证处理过滤器,这是一个抽象类,定义了认证处理的过程。是一个模板类。默认的实现为 UsernamePasswordAuthenticationFilter
,根据用户名、密码进行身份验证,如果需要自定义身份验证,比如手机验证码登录,就需要继承该类。
授权
当用户访问 Spring Security 中一个受保护的资源时,需要使用投票器和表决机制,投票器根据用户的角色投出赞成或者反对票,表决方式则根据投票器的结果进行表决。
AccessDecisionManager
访问决策管理器,AccessDecisionManager
由AbstractSecurityInterceptor
调用。AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的多个 AccessDecisionVoter,Voter 将会被用来对Authentication是否有权访问受保护对象进行投票, AccessDecisionManager 根据投票结果,做出最终决策。
AccessDecisionManager 访问决策管理器还有三个子类决策器,分别是:
- AffirmativeBased:存在多个投票器时,有一个投票器同意,则请求就允许访问,也就是一票通过;默认使用的决策器。
- UnanimousBased:存在多个投票器时,如果有投票器拒绝,则请求不允许访问,也就是一票否决。
- ConsensusBased:存在多个投票器时,大多数投票器同意。则请求就允许访问,也就是少数服从多数。如果是平局,则看 allowIfEqualGrantedDeniedDecisions 的值来判断是否通过,在默认情况下
allowIfEqualGrantedDeniedDecisions
值是true,也就是说平票的情况下,请求允许访问。
注意:不论是哪个决策器,如果所有投票器全部弃权则表示通过。
AccessDecisionVoter
翻译为:投票机制/投票器。在 Spring Security 中,投票机制是由 AccessDecisionVoter 接口来定义的,它有许多的实现:
实现有好多种,我们可以选择其中一种或多种投票机制,也可以自定义投票机制,默认的投票机制是 WebExpressionVoter。
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
复制代码
- 三个常量含义分别为:1 表示同意;0 表示弃权;-1 表示拒绝。
- 两个 supports 方法用来判断投票器是否支持当前请求。
-
- vote 则是具体的投票方法;在不同的实现类中实现。authentication 表示当前登录主体;object 表示正在调用的受保护接口;attributes 表示当前所访问的接口所需要的角色集合。
- RoleVoter:判断当前请求是否具备该接口所需要的角色。
- RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承。
- WebExpressionVoter:基于表达式权限控制
- Jsr250Voter:处理 Jsr-250 权限注解的投票器,如
@PermitAll
,@DenyAll
等 - PreInvocationAuthorizationAdviceVoter:使用 @PreFilter 和 @PreAuthorize 注解处理的权限,通过 PreInvocationAuthorizationAdvice 来授权。
AbstractSecurityInterceptor
根据注释翻译了一下:为安全对象实现安全拦截的抽象类,干的事情说白点就是对未放行的资源,根据用户的权限来控制是否能访问的拦截器。由于这是一个抽象类,所以只定义了一些逻辑方法,具体执行都是子类去调用的。
FilterSecurityInterceptor
从 FilterChainProxy 章节中的截图来看,FilterSecurityInterceptor 位于 Spring Security Filter Chain 中的最后一个 Filter。这是一个过滤器,它会拦截HTTP请求,进行鉴权处理。
MethodSecurityInterceptor
它还实现了 MethodInterceptor,所以这是一个方法拦截器,基于 Spring AOP 实现了方法拦截,对方法进行鉴权处理。比如在方法上标注了@RolesAllowed
、@PermitAll
、@PreAuthorize
等等注解。
AspectJMethodSecurityInterceptor
继承了 MethodSecurityInterceptor,基于 Aspectj 实现方法拦截。
区别
- 一个是过滤器,一个是方法拦截器,所以他们拦截对象是不同,一个是拦截Http请求,一个是拦截方法。
- 两者都调用了父类的beforeInvocation方法,但是传入的参数是不一样的,FilterSecurityInterceptor传入的是FilterInvocation,MethodInterceptor传入的是MethodInvocation。
- 两者维护的 SecurityMetadataSource 不一样,MethodSecurityInterceptor 中维护的是MethodSecurityMetadataSource,FilterSecurityInterceptor维护的是 FilterInvocationSecurityMetadataSource。
SecurityMetadataSource
获取授权配置的接口。可以自定义实现该接口,比如从数据库中加载ConfigAttribute。在 Spring Security中,给该接口提供了两个子类,继承图如下:
MethodSecurityMetadataSource
:由Spring Security Core
定义,用于表示安全对象是方法调用(MethodInvocation
)的安全元数据源;存放的是所有类和方法,会根据当前执行的类和方法,去内存中遍历,查询到当前执行方法配置的权限注解,然后对其进行授权判断。
FilterInvocationSecurityMetadataSource
:由Spring Security Web
定义,用于表示安全对象是Web
请求(FilterInvocation
)的安全元数据源;存放的是 HttpSecurity 配置类中配置的授权规则。
配置如下:
http.authorizeRequests()
// 如果用户具备 admin 权限,就允许访问。
.antMatchers("/cxyxj/**").hasAuthority("ROLE_admin")
// 如果用户具备给定权限中某一个,就允许访问。
.antMatchers("/admin/demo").hasAnyAuthority("ROLE_admin", "ROLE_System")
// 如果用户具备 user 权限,就允许访问。注意不需要手动写 ROLE_ 前缀,写了会报错
.antMatchers("/security/**").hasRole("user")
//如果请求是指定的 IP 就允许访问。
.antMatchers("/admin/demo").hasIpAddress("192.168.64.5")
.anyRequest() //其他请求
.authenticated(); //需要认证才能访问
复制代码