提示
Spring Security 源码的接口名和方法名都很长,看源码的时候要见名知意,有必要细看接口名和方法名,另外可以借助流程图,调试追踪代码,有助于理解学习!
Apache Shiro 和 Spring Security
Spring Security 是一个**可高度可定制的身份验证(认证)和访问控制(授权)框架,**它是用于保护基于 Spring 的应用程序的实际标准,相比与另外一个安全框架 Shiro,它提供了更丰富的功能,社区资源也比 Shiro 丰富。
- Shiro 轻量级,不依赖 Spring,是第三方框架,简单而灵活,可以用于非 Web 环境!
- Security 重量级,依赖 Spring,控制粒度更细,老版本不能脱离 Web,新版本可以
- Spring Boot 2 默认使用 Security 5,要求 JDK 至少是 8
- Spring Boot 3 默认使用 Security 6,要求 JDK 至少是 17
- Security 5 和 Security 6 的区别之一就是 5 到 6 废弃了WebSecurityConfigurerAdapter 类,在 Security 5 编写配置类需要继承 WebSecurityConfigurerAdapter 并重写某些方法,但是在 Security 6 已经不需要了
Spring Security:升级已弃用的 WebSecurityConfigurerAdapter - spring 中文网
从 Spring Security 5 迁移到 Spring Security 6/Spring Boot 3 - spring 中文网
认证和授权
认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作
注:Authentication 和 Authorization 拼写很像,有必要分清,后面看源代码方便!
RBAC
Role-Based Access Control 基于角色的访问控制

把权限打包给角色(角色拥有一组权限),给用户分配角色(用户拥有多个角色)
最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
Demo
环境
Spring Boot:2.7.2
SpringSecurity:5+
JDK:1.8
Controller
|
|
可以直接访问

引入 Spring Security
|
|
再次访问时默认出现了该登陆界面,默认用户是 user,密码默认在控制台生成 ,成功登陆后才会放行。Spring Security 默认拦截了除登录、退出之外的所有请求。显示的登录和退出表单是 Security 默认生成的。
(前端页面样式 bootstrap.min.css 是一个 CDN 地址,需要魔法,所以加载很慢)

初探 Security 原理
**Spring Security 底层是基于 Servlet 的过滤器链,默认共有 16 个过滤器,这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor**负责权限授权。

认证授权图示

图中涉及的类和接口
- UsernamePasswordAuthenticationFilter 类
抽象类 AbstractAuthenticationProcessingFilter 的子类,是常用的用户名和密码认证方式的主要处理类,该类中将用请求信息封装为初步的 Authentication(Authentication 实际上是一个接口,它的实现类是 UsernamePasswordAuthenticationToken),此时最多只有用户名和密码,在登录认证成功之后又会生成一个包含用户权限等信息的更全面的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中。
- AuthenticationManager 接口
用来处理 Authentication 请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象。
|
|
- ProviderManager 类
|
|
AuthenticationManager 接口的实现类,AuthenticationManager 接口不直接自己处理认证请求,而是委托给ProviderManager所配置的AuthenticationProvider 列表,而 ProviderManager 的作用就是管理这个 AuthenticationProvider 列表,管理的方式是通过 for 循环去遍历该列表,因为不同的登录逻辑(表单登录、qq 登录、邮箱登录)是不一样的,那么AuthenticationProvider 列表就要支持不同的 Authentication。
信息验证的逻辑都是在AuthenticationProvider**里面,会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 **ProviderNotFoundException**。
- AuthenticationProvider 接口
该接口被实现为抽象类 AbstractUserDetailsAuthenticationProvider,然后 DaoAuthenticationProvider 类继承该抽象类,并拥有一个 UserDetailsService 的变量
|
|
- UserDetailsService 接口
|
|
加载用户数据的核心接口,可以自定义从数据库加载数据或者从内存加载临时用户(InMemory),登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 类型的用户信息进行认证,认证通过后会将用户信息封装为 UserDetails 接口的实现类 User 并赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。
- InMemoryUserDetailsManager 类
该类是UserDetailsService 接口的实现类,作用是从内存中加载用户,也就是说用户是在代码中提前写好的,程序运行后被加载到内存,不是从数据库中取的没有持久化,如果是从数据库加载用户就不用这个类了。该类有一个方法是
|
|
- UserDetails 接口
提供用户核心信息的接口,通过 UserDetailsService 的 loadUserByUsername() 方法获取,然后将该 UserDetails 赋给认证通过的 Authentication 的 principal。
|
|
- User 类
|
|
UserDetails 接口的实现类,User 类也是 security 的默认用户类,我们可以继承该类对其方法重写
- SecurityContextHolder 类
用来保存 SecurityContext 的,SecurityContext 中含有当前正在访问系统的用户的详细信息。默认情况下,SecurityContextHolder 使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext。因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的。这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。
流程总结
用户填写的用户名密码传到后端,进入 Security 的过滤器链进行验证,验证流程为:
进入 UsernamePasswordAuthenticationFilter,里面有一个 attemptAuthentication 方法,该方法会生成一个 UsernamePasswordAuthenticationToken,也就是一个凭证 Authentication,这个 Authentication 只包含了用户名、密码这些基础信息,没有权限等其他信息,然后 Authentication 作为参数被传到 AuthenticationManager 接口的实现类 ProviderManager 类的 authenticate 方法进行认证,ProviderManager 类中有一个 List
DaoAuthenticationProvider 内部的认证逻辑:它有一个 retrieveUser 方法,该方法调用 UserDetailsService().loadUserByUsername 从数据库获取用户信息,并返回一个 UserDetails 类型的对象,它含有用户的详细信息,如用户权限等等,然后将 UserDetails 对象和 Authentication 校验,成功后会把 UserDetails 中的信息填入到 Authentication,一个最终的 Authentication 就产生了,它会被保存到安全上下文中!