Spring Security架构和核心类一览
前言
又到了恶心人环节, 概念
Spring Security框架
spring 官方给出了几张图片说明 spring security 是什么? 我就挑出两张看看吧
全文大意是说, spring security是一个过滤器, 借助 DelegatingFilterProxy
搭建了一条servlet生命周期 和 spring bean 之间的桥梁
说一千道一万, 直接看代码完事:
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() throws Exception {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
DelegatingFilterProxy delegatingFilterProxy = new DelegatingFilterProxy();
delegatingFilterProxy.setTargetFilterLifecycle(true);
delegatingFilterProxy.setBeanName("myFilter");
filterRegistrationBean.setFilter(myFilter);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
这里的myFilter
是 servlet的过滤器, DelegatingFilterProxy
是Spring提供的一个类
这样我们可以在 spring Bean 中管理 servlet过滤器
FilterChainProxy
是一个Bean
, 被包装在DelegatingFilterProxy
中, 而FilterChainProxy
类似于一个集合, 每个元素是一个SecurityFilterChain
, 而每个SecurityFilterChain
内部又有一堆Filter
, 这些过滤器就是我们前面说的LogoutFilter
UsernamePasswordAuthenticationFilter
等等
你可以把
SecurityFilterChain
当做一个班级,FilterChainProxy
是一个学校
你可以看到左边SecurityFilterChain
头上的"班牌", 但该"班牌"的作用和实际的班牌还有一定的差别
比如:
校长说, 初一年段, 在实际指初一整个年段, 在 spring security中值 初一(1)班
它只会匹配第一个SecurityFilterChain
, 后续的SecurityFilterChain
如果还匹配, 它也不会执行
文档里面还说, spring security过滤器的顺序非常重要
认证核心代码
SecurityContextHolder
这个类是spring security用于存储用户身份认证完毕后存放认证信息的地方
底层真正存储信息的方式默认是ThreadLocal线程绑定方式
小白: "那本次请求结束咋办?" 小黑: "可以借助 session , 在请求结束前从 ThreadLocal中读取到 session中, 在请求到来时, 从session中读取信息到ThreadLocal中"
加载SecurityContextHolder
他的内部使用HttpSessionSecurityContextRepository implements SecurityContextRepository
, 将会从session中读取SecurityContextHolder
清除SecurityContextHolder
小白: "等等, 你好像还忘讲了什么?"
小黑: "你说的是上面这个函数吧? 因为
SecurityContextHolder
默认是ThreadLocal
, 如果在线程中再创建一个子线程, 那么就无法读取到当前线程的SecurityContextHolder
了, 所以要改, 方法也在下面这张图片"
小黑: "看看上面的图片, 你告诉我, 要改变上面函数的if判断, 要改哪个?"
小白: "给
spring.security.strategy
设置一个系统属性就行, 直接加在启动类那里"
小黑: "是的, 但是spring security还提供了另一种方法
DelegatingSecurityContextExecutorService
"
GrantedAuthority
GrantedAuthority
实例是授予用户的高级权限。两个例子是角色和范围。
你可以使用 Authentication.getAuthorities()
获得 GrantedAuthority
的集合, 而这个集合就是当前用户的所有权限
authorities
通常是角色, ROLE_ADMINISTRATOR
or ROLE_HR_SUPERVISOR
当你使用username/password
验证身份授权GrantedAuthority
实例时, 通常使用 UserDetailsService
去加载
GrantedAuthority
是应用层权限, 而不是针对某个对象的, 就像你不能给Employee
的某个ID
添加权限一样
AuthenticationManager
AuthenticationManager
决定了Spring Security的过滤器如何执行, 同时他也是认证的核心类, 传递到函数中的Authentication
就只有username
和password
, 等认证成功, 将填充Authentication
对象并返回, 否则将抛出 AuthenticationException
异常
它有很多实现类, 其中一个最重要的类是ProviderManager
ProviderManager
ProviderManager
相当于一个集合, 集合的每一个元素都是AuthenticationProvider
类, 这些AuthenticationProvider
是最终认证的地方, 每一个AuthenticationProvider
都是相互隔离的, 至少前一个认证器不能决定下一个认证器是哪一个
如果轮询结束后, 没有一个AuthenticationProvider
被执行, 则会抛出 ProviderNotFoundException
在上图中看到, 有一个parent
属性, 该属性说明AuthenticationManager
可以有一个公共的parent
类
ProviderManager
还提供了清除敏感信息的功能, 比如删除掉密码之类
AuthenticationProvider
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider
提供了一个supports
函数, 判断当前Authentication
是否支持 AuthenticationProvider
, 通过supports
函数判断匹配, 对应的AuthenticationProvider
才会执行
比如DaoAuthenticationProvider
的supports
函数需要下面这个类
又比如JwtAuthenticationProvider
的supports
函数需要下面的这个类
说白了都是看Authentication authenticate(Authentication authentication) throws AuthenticationException;
这个函数传入进来的类型匹配就行
UsernamePasswordAuthenticationFilter
spring security有很多过滤器, UsernamePasswordAuthenticationFilter
就是其一
单独拿出讲的原因是经常用, 要么重写, 要么调试, 主要的认证过程就看它了
核心代码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
将request
中的参数提取出来保存到 Authentication
中, 返回给认证器认证, 就这么一步, 如果你是前后端分离, 就有可能需要重写该方法
DaoAuthenticationProvider
这个认证器提供了密码验证方法additionalAuthenticationChecks
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException();
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword(/**/))) {
throw new BadCredentialsException(/**/);
}
}
认证成功判断是否更新密码加密方法的函数
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
找不到用户, 为了防止被旁道攻击而调用的方法
小黑: "因为加密算法需要大量系统资源, 所以拿到用户比较密码前需要 encode 一次, 而这个函数需要时间(大概1秒, 当然你可以认为100年这样更好理解), 如果从数据库中拿不到数据, 正常情况直接返回, 如果拿到数据, 需要等待'100年', 黑客的孙子就可以根据等待时间知道数据库中有该用户名, 可以进行撞库看看."
小白: "等等, 数据库中不都是已加密的密码了吗? 哦, 用户输入的密码不是....需要加密一次"
获取数据库中用户的方法retrieveUser
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
这行代码涉及下一个要讲的接口
UserDetailsService
默认spring security保存在内存中, 如果你需要改从数据库中拿到用户, 就需要重写UserDetailsService
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
平时都是这么玩的
PasswordEncoder
spring security所有密码加密方式的实现都跟这个接口相关
这里你只要关注B开头的,还有D开头的两个加密算法类就行了。(手冷不打字了,全部都用说的。)
public interface PasswordEncoder {
// 加密方法。
String encode(CharSequence rawPassword);
// 密码匹配方法。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 判断加密方式是否需要升级方法。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
spring security默认使用DelegatingPasswordEncoder
, 这个类内部可以存放多种security支持的加密方法。相当于一个加密算法的集合。
使用这个接口之后,密码前面都需要加上ID。也就是{noop}123456
{bcrypt}456789
, 前面的花括号部分就是他们的ID。如果我们数据库都存放着这一种密码的话, DelegatingPasswordEncoder
会自动根据前面的ID执行跟ID所匹配的加密算法。
BCryptPasswordEncoder
比较推荐使用。
在实际的项目中,我们通常都是要么配置一个DelegatingPasswordEncoder
, 这么使用默认的也是DelegatingPasswordEncoder
而且也不是直接使用的这个DelegatingPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() throws Exception {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
总结
剩下的还有一些涉及到登录成功后怎么做, 登录失败后怎么做, 出现认证异常怎么做, 出现拒绝访问异常怎么做
这每一个背后都会有一个接口, 提供功能, 都会有默认实现, 这里讲的全是传统web, 以后前后端分离会讲
我想想还有什么没想到的...OAuth
? 还是 JWT
相关? 忘了
全文重点就官网的那两张图吧, 其他核心类后面都会碰到, 看着看着就熟了
转载自:https://juejin.cn/post/7182504387875438649