目录
提示
Apache Shiro和Spring Security
认证和授权
RBAC
Demo
环境
Controller
引入Spring Security
初探Security原理
认证授权图示编辑
图中涉及的类和接口
流程总结
提示
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
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/query")
public String queryInfo(){
return "I am a admin";
}
}
可以直接访问
引入Spring Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
再次访问时默认出现了该登陆界面,默认用户是user,密码默认在控制台生成 ,成功登陆后才会放行。Spring Security默认拦截了除登录、退出之外的所有请求。显示的登录和退出表单是Security默认生成的。
(前端页面样式bootstrap.min.css是一个CDN地址,需要魔法,所以加载很慢)
初探Security原理
Spring Security底层是基于Servlet的过滤器链,默认共有16个过滤器,这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter
负责登录认证,FilterSecurityInterceptor
负责权限授权。
认证授权图示
图中涉及的类和接口
UsernamePasswordAuthenticationFilter类
抽象类AbstractAuthenticationProcessingFilter的子类,是常用的用户名和密码认证方式的主要处理类,该类中将用请求信息封装为初步的Authentication(Authentication实际上是一个接口,它的实现类是UsernamePasswordAuthenticationToken),此时最多只有用户名和密码,在登录认证成功之后又会生成一个包含用户权限等信息的更全面的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中。
AuthenticationManager接口
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
用来处理Authentication请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象。
ProviderManager类
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean
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的变量public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
- UserDetailsService接口
加载用户数据的核心接口,可以自定义从数据库加载数据或者从内存加载临时用户(InMemory),登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 类型的用户信息进行认证,认证通过后会将用户信息封装为UserDetails接口的实现类User并赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
InMemoryUserDetailsManager类
该类是UserDetailsService接口的实现类,作用是从内存中加载用户,也就是说用户是在代码中提前写好的,程序运行后被加载到内存,不是从数据库中取的没有持久化,如果是从数据库加载用户就不用这个类了。该类有一个方法是private User createUserDetails(String name, UserAttribute attr),返回值是User类型
UserDetails接口
public interface UserDetails extends Serializable
提供用户核心信息的接口,通过 UserDetailsService 的 loadUserByUsername() 方法获取,然后将该 UserDetails 赋给认证通过的 Authentication 的 principal。
User类
public class User implements UserDetails, CredentialsContainer
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<AuthenticationProvider> 列表(AuthenticationProvider是接口,AbstractUserDetailsAuthenticationProvider是接口的抽象类,DaoAuthenticationProvider是实现类),该列表存放着不同的登录逻辑AuthenticationProvider,通过for循环去遍历该列表,信息验证的逻辑都是在AuthenticationProvider里面,会依次使用每一AuthenticationProvider进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该AuthenticationProvider已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。
DaoAuthenticationProvider内部的认证逻辑:它有一个retrieveUser方法,该方法调用UserDetailsService().loadUserByUsername从数据库获取用户信息,并返回一个UserDetails类型的对象,它含有用户的详细信息,如用户权限等等,然后将UserDetails对象和Authentication校验,成功后会把UserDetails中的信息填入到Authentication,一个最终的Authentication就产生了,它会被保存到安全上下文中!