已经写了好几篇关于 Spring Security 的文章了,相信很多读者还是对 Spring Security 的云里雾里的。这是因为对 Spring Security 中的对象还不了解。本文就来介绍介绍一下常用对象。
用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保存在会话中。Spring Security 提供会话管理,认证通过后将身份信息放入 SecurityContextHolder 上下文,SecurityContext 与当前线程进行绑定,方便获取用户身份。
- // 获取当前登录的用户信息
- Authentication authentication =
- SecurityContextHolder.getContext().getAuthentication();
- 复制代码
认证管理器,AuthenticationManager 是认证相关的核心接口,是发起认证的入口,用于处理认证请求。接口只提供了一个认证方法,方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。最常见的实现是ProviderManager
。
- public interface AuthenticationManager {
- Authentication authenticate(Authentication var1) throws AuthenticationException;
- }
- 复制代码
提供商管理器,ProviderManager
是 AuthenticationManager
的一个实现类,提供了基本的认证逻辑和方法。它其中包含了一个 List 的 AuthenticationProvider
的属性,该属性存放多种认证方式!为什么需要这个属性呢?当Spring Security
默认提供的认证方式不能满足需求时,就可以通过 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(身份验证)
接口是 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
进行验证。
这个接口定义了用户的核心信息,比如用户名、密码、账号是否过期、是否锁定等!默认实现类org.springframework.security.core.userdetails.User。在 Spring Security 中,如果自定义认证逻辑时,需要实现该接口进行扩展,来保存自己系统的用户信息。接口定义如下方法:
方法 | 描述 |
---|---|
getAuthorities | 获取用户权限 |
getPassword | 获取用户密码 |
getUsername | 获取用户名 |
isAccountNonExpired | 账户是否未过期,true:未过期,false:过期 |
isAccountNonLocked | 账户是否未锁定,true:未锁定,false:锁定 |
isCredentialsNonExpired | 凭证(密码)是否未过期,true:未过期,false:过期 |
isEnabled | 账户是否启用,true:启用,false:禁用 |
一个正常能登录的账号,四个状态都是为 true 的。
在 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 接口用于执行密码的单向转换,以便安全地存储密码。
身份验证成功后,发布一个名为InteractiveAuthenticationSuccessEvent
的事件通知给到应用上下文,用于告知身份验证已经成功。
在 Spring Security 的默认配置中,将创建一个名为 springSecurityFilterChain 的 servlet 过滤器作为bean。默认情况下,Spring Security 内置了一个过滤链,链中有 15 个过滤器。
想要了解更多请前往: 深入理解 FilterChainProxy【源码篇】
ExceptionTranslationFilter 异常转换过滤器位于整个 springSecurityFilterChain 的后方,用来转换整个链路中出现的异常。此过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,一般处理两大类异常:AccessDeniedException 已登录无权限访问异常和 AuthenticationException 未认证访问异常。
用来给http响应添加一些Header,比如X-Frame-Options, X-XSS- Protection*,X-Content-Type-Options.
用于防止csrf攻击(跨站点请求伪造(Cross- site request forgery))。
处理注销的过滤器。
内部维护了一个RequestCache,用于缓存request请求。
对ServletRequest进行了一次包装,使得request 具有更加丰富的API。
和session相关的过滤器,内部维护了一个 SessionAuthenticationStrategy,两者组合使用,常用来防止会话固定攻击保护( session- fixation protection attack ),以及限制同一用户开启多个会话的数量。
匿名身份过滤器,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
表单提交了username和password参数,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的。在表单认证的流程中,这是最最关键的过滤器。
翻译为:抽象身份验证处理过滤器,这是一个抽象类,定义了认证处理的过程。是一个模板类。默认的实现为 UsernamePasswordAuthenticationFilter
,根据用户名、密码进行身份验证,如果需要自定义身份验证,比如手机验证码登录,就需要继承该类。
当用户访问 Spring Security 中一个受保护的资源时,需要使用投票器和表决机制,投票器根据用户的角色投出赞成或者反对票,表决方式则根据投票器的结果进行表决。
访问决策管理器,AccessDecisionManager
由AbstractSecurityInterceptor
调用。AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的多个 AccessDecisionVoter,Voter 将会被用来对Authentication是否有权访问受保护对象进行投票, AccessDecisionManager 根据投票结果,做出最终决策。
AccessDecisionManager 访问决策管理器还有三个子类决策器,分别是:
allowIfEqualGrantedDeniedDecisions
值是true,也就是说平票的情况下,请求允许访问。注意:不论是哪个决策器,如果所有投票器全部弃权则表示通过。
翻译为:投票机制/投票器。在 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);
- }
- 复制代码
@PermitAll
,@DenyAll
等根据注释翻译了一下:为安全对象实现安全拦截的抽象类,干的事情说白点就是对未放行的资源,根据用户的权限来控制是否能访问的拦截器。由于这是一个抽象类,所以只定义了一些逻辑方法,具体执行都是子类去调用的。
从 FilterChainProxy 章节中的截图来看,FilterSecurityInterceptor 位于 Spring Security Filter Chain 中的最后一个 Filter。这是一个过滤器,它会拦截HTTP请求,进行鉴权处理。
它还实现了 MethodInterceptor,所以这是一个方法拦截器,基于 Spring AOP 实现了方法拦截,对方法进行鉴权处理。比如在方法上标注了@RolesAllowed
、@PermitAll
、@PreAuthorize
等等注解。
继承了 MethodSecurityInterceptor,基于 Aspectj 实现方法拦截。
获取授权配置的接口。可以自定义实现该接口,比如从数据库中加载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(); //需要认证才能访问
- 复制代码