松哥最近在研究 Spring Security 源码,发现了不少好玩的代码,抽空写几篇文章和小伙伴们分享一下。java
不少人吐槽 Spring Security 比 Shiro 重量级,这个重量级不是凭空来的,重量有重量的好处,就是它提供了更为强大的防御功能。算法
好比松哥最近看到的一段代码:数据库
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); } }
这段代码位于 DaoAuthenticationProvider 类中,为了方便你们理解,我来简单说下这段代码的上下文环境。后端
当用户提交用户名密码登陆以后,Spring Security 须要根据用户提交的用户名去数据库中查询用户,这块若是你们不熟悉,能够参考松哥以前的文章:设计模式
查到用户对象以后,再去比对从数据库中查到的用户密码和用户提交的密码之间的差别。具体的比对工做,能够参考Spring Boot 中密码加密的两种姿式!一文。跨域
而上面这段代码就是 Spring Security 根据用户登陆时传入的用户名去数据库中查询用户,并将查到的用户返回。方法中还有一个 authentication 参数,这个参数里边保存了用户登陆时传入的用户名/密码信息。缓存
那么这段代码有什么神奇之处呢?安全
咱们来一行一行分析。session
源码梳理
1
首先方法一进来调用了 prepareTimingAttackProtection 方法,从方法名字上能够看出,这个是为计时攻击的防护作准备,那么什么又是计时攻击呢?别急,松哥一会来解释。咱们先来吧流程走完。prepareTimingAttackProtection 方法的执行很简单,以下:框架
private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); } }
该方法就是将常量 USER_NOT_FOUND_PASSWORD 使用 passwordEncoder 编码以后(若是不了解 passwordEncoder,能够参考 Spring Boot 中密码加密的两种姿式!一文),将编码结果赋值给 userNotFoundEncodedPassword 变量。
2
接下来调用 loadUserByUsername 方法,根据登陆用户传入的用户名去数据库中查询用户,若是查到了,就将查到的对象返回。
3
若是查询过程当中抛出 UsernameNotFoundException 异常,按理说直接抛出异常,接下来的密码比对也不用作了,由于根据用户名都没查到用户,此次登陆确定是失败的,没有必要进行密码比对操做!
可是你们注意,在抛出异常以前调用了 mitigateAgainstTimingAttack 方法。这个方法从名字上来看,有缓解计时攻击的意思。
咱们来看下该方法的执行流程:
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); } }
能够看到,这里首先获取到登陆用户传入的密码即 presentedPassword,而后调用 passwordEncoder.matches 方法进行密码比对操做,原本该方法的第二个参数是数据库查询出来的用户密码,如今数据库中没有查到用户,因此第二个参数用 userNotFoundEncodedPassword 代替了,userNotFoundEncodedPassword 就是咱们一开始调用 prepareTimingAttackProtection 方法时赋值的变量。这个密码比对,从一开始就注定了确定会失败,那为何还要比对呢?
计时攻击
这就引入了咱们今天的主题--计时攻击。
计时攻击是旁路攻击的一种,在密码学中,旁道攻击又称侧信道攻击、边信道攻击(Side-channel attack)。
这种攻击方式并不是利用加密算法的理论弱点,也不是暴力破解,而是从密码系统的物理实现中获取的信息。例如:时间信息、功率消耗、电磁泄露等额外的信息源,这些信息可被用于对系统的进一步破解。
旁路攻击有多种不一样的分类:
- 缓存攻击(Cache Side-Channel Attacks),经过获取对缓存的访问权而获取缓存内的一些敏感信息,例如攻击者获取云端主机物理主机的访问权而获取存储器的访问权。
- 计时攻击(Timing attack),经过设备运算的用时来推断出所使用的运算操做,或者经过对比运算的时间推定数据位于哪一个存储设备,或者利用通讯的时间差进行数据窃取。
- 基于功耗监控的旁路攻击,同一设备不一样的硬件电路单元的运做功耗也是不同的,所以一个程序运行时的功耗会随着程序使用哪种硬件电路单元而变更,据此推断出数据输出位于哪个硬件单元,进而窃取数据。
- 电磁攻击(Electromagnetic attack),设备运算时会泄漏电磁辐射,通过得当分析的话可解析出这些泄漏的电磁辐射中包含的信息(好比文本、声音、图像等),这种攻击方式除了用于密码学攻击之外也被用于非密码学攻击等窃听行为,如TEMPEST 攻击。
- 声学密码分析(Acoustic cryptanalysis),经过捕捉设备在运算时泄漏的声学信号捉取信息(与功率分析相似)。
- 差异错误分析,隐密数据在程序运行发生错误并输出错误信息时被发现。
- 数据残留(Data remanence),可以使理应被删除的敏感数据被读取出来(例如冷启动攻击)。
- 软件初始化错误攻击,现时较为少见,行锤攻击(Row hammer)是该类攻击方式的一个实例,在这种攻击实现中,被禁止访问的存储器位置旁边的存储器空间若是被频繁访问将会有状态保留丢失的风险。
- 光学方式,即隐密数据被一些视觉光学仪器(如高清晰度相机、高清晰度摄影机等设备)捕捉。
全部的攻击类型都利用了加密/解密系统在进行加密/解密操做时算法逻辑没有被发现缺陷,可是经过物理效应提供了有用的额外信息(这也是称为“旁路”的原因),而这些物理信息每每包含了密钥、密码、密文等隐密数据。
而上面 Spring Security 中的那段代码就是为了防止计时攻击。
具体是怎么作的呢?假设 Spring Security 从数据库中没有查到用户信息就直接抛出异常了,没有去执行 mitigateAgainstTimingAttack 方法,那么黑客通过大量的测试,再通过统计分析,就会发现有一些登陆验证耗时明显少于其余登陆,进而推断出登陆验证时间较短的都是不存在的用户,而登陆耗时较长的是数据库中存在的用户。
如今 Spring Security 中,经过执行 mitigateAgainstTimingAttack 方法,不管用户存在或者不存在,登陆校验的耗时不会有明显差异,这样就避免了计时攻击。
可能有小伙伴会说,passwordEncoder.matches 方法执行能耗费多少时间呀?这要看你怎么计时了,时间单位越小,差别就越明显:毫秒(ms)、微秒(µs)、奈秒(ns)、皮秒(ps)、飛秒(fs)、阿秒(as)、仄秒(zs)。
另外,Spring Security 为了安全,passwordEncoder 中引入了一个概念叫作自适应单向函数,这种函数故意执行的很慢而且消耗大量系统资源,因此很是有必要进行计时攻击防护。
关于自适应单向函数,这是另一个故事了,松哥抽空再和小伙伴们聊~
好啦,今天就先和小伙伴们聊这么多,小伙伴们决定有收获的话,记得点个在看鼓励下松哥哦~
不知不觉,Spring Security 系列已经连载了 49 篇啦,感兴趣的小伙伴也能够看看本系列其余文章哦:
- 挖一个大坑,Spring Security 开搞!
- 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
- 手把手教你定制 Spring Security 中的表单登陆
- Spring Security 作先后端分离,咱就别作页面跳转了!通通 JSON 交互
- Spring Security 中的受权操做原来这么简单
- Spring Security 如何将用户数据存入数据库?
- Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
- Spring Boot + Spring Security 实现自动登陆功能
- Spring Boot 自动登陆,安全风险要怎么控制?
- 在微服务项目中,Spring Security 比 Shiro 强在哪?
- SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
- Spring Security 中如何快速查看登陆用户 IP 地址等信息?
- Spring Security 自动踢掉前一个登陆用户,一个配置搞定!
- Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?
- Spring Security 自带防火墙!你都不知道本身的系统有多安全!
- 什么是会话固定攻击?Spring Boot 中要如何防护会话固定攻击?
- 集群化部署,Spring Security 要如何处理 session 共享?
- 松哥手把手教你在 SpringBoot 中防护 CSRF 攻击!so easy!
- 要学就学透彻!Spring Security 中 CSRF 防护源码解析
- Spring Boot 中密码加密的两种姿式!
- Spring Security 要怎么学?为何必定要成体系的学习?
- Spring Security 两种资源放行策略,千万别用错了!
- 松哥手把手教你入门 Spring Boot + CAS 单点登陆
- Spring Boot 实现单点登陆的第三种方案!
- Spring Boot+CAS 单点登陆,如何对接数据库?
- Spring Boot+CAS 默认登陆页面太丑了,怎么办?
- 用 Swagger 测试接口,怎么在请求头中携带 Token?
- Spring Boot 中三种跨域场景总结
- Spring Boot 中如何实现 HTTP 认证?
- Spring Security 中的四种权限控制方式
- Spring Security 多种加密方案共存,老破旧系统整合利器!
- 神奇!本身 new 出来的对象同样也能够被 Spring 容器管理!
- Spring Security 配置中的 and 到底该怎么理解?
- 一文搞定 Spring Security 异常处理机制!
- 写了这么多年代码,这样的登陆方式仍是头一回见!
- Spring Security 居然能够同时存在多个过滤器链?
- Spring Security 能够同时对接多个用户表?
- 在 Spring Security 中,我就想从子线程获取用户登陆信息,怎么办?
- 深刻理解 FilterChainProxy【源码篇】
- 深刻理解 SecurityConfigurer 【源码篇】
- 深刻理解 HttpSecurity【源码篇】
- 深刻理解 AuthenticationManagerBuilder 【源码篇】
- 花式玩 Spring Security ,这样的用户定义方式你可能没见过!
- 深刻理解 WebSecurityConfigurerAdapter【源码篇】
- 盘点 Spring Security 框架中的八大经典设计模式
- Spring Security 初始化流程梳理
- 为何你使用的 Spring Security OAuth 过时了?松哥来和你们捋一捋!
- 一个诡异的登陆问题