Spring Security 最核心的两个功能就是验证和鉴权。
3.1 Spring Security 的原理
认证就是验证你是谁,鉴权就是你能干什么。权限系统的设计也必须按照这两步来进行思考和设计。
Spring Security 本质就是一系列的拦截器组成的拦截器链。(如下图)
3.2 Spring Boot 整合 Spring Security 的实现
3.2.1 认证
我们从上图可以看出,认证跟 Spring Security 提供的 UserPasswordAuthenticationFilter 有关。
我们对这个类的源码进行分析一下。
通过源码我们可以看出,它主要拦截我们的登录请求并拿到我们的登录名和密码,然后生成通过用户名和密码生成一个未认证的 UsernamePasswordAuthenticationToken 对象,等待我们验证。就你登录请求过来,我这个拦截器就会拦截到然后拿到你的用户名、密码,然后封装成一个为认证的东西,方便我后边对你进行认证。
3.2.2 拦截器源码分析
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
// 你一按登录就会请求认证
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 判断是否为POST方式的请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 我们请求的登录中获取到用户名和密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
// 进一步验证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
这个未认证的 UsernamePasswordAuthenticationToken 的构造方法,我们通过源码就知道它到底是个什么东西。
UsernamePasswordAuthenticationToken 源码:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
//当前登录的用户名具有什么权限,由于刚进行认证,这里的权限我们赋值为空
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
// 当前登录的用户名是否已通过验证,由于这里还没进行认证,所以我们赋值为为认证
this.setAuthenticated(false);
}
接着 AuthenticationManager 就会对我们上面的这个 UsernamePasswordAuthenticationToken 来进行认证。但 AuthenticationManager 并不是真正干活的人,真正做事的是 ProviderManager。我们通过源码可以看出 ProviderManager 继承于我的 AuthenticationManager。然后它会选择合适的 Provider 对我们的 UsernamePasswordAuthenticationToken 来进行认证。这个因为是 UsernamePasswordAuthenticationToken,所以选择的是 DaoAuthenticationProvider。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
Iterator var6 = this.getProviders().iterator();
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
// 1.判断是否有provider支持该Authentication
if (provider.supports(toTest)) {
result = provider.authenticate(authentication);
}
}
}
开始对登录请求进行认证。去调用自己实现的 UserDetailsService,返回 UserDetails。
对 UserDetails 的信息进行校验,主要是帐号是否被冻结,是否过期等对密码进行检查,这里调用了 PasswordEncoder 检查 UserDetails 是否可用。这个时候就返回经过认证的 Authentication。
成功认证的话就会去调用我的 successHandler 返回,失败就去调用我的 failHandler。
3.2.3 认证的总体思路流程
根据上面的思路,整个代码来具体的讲解一下是如何实现的。
首先我们要自定义 DaoAuthenticationProvider 对我们的 UsernamePasswordAuthenticationToken 来进行认证。我们在 SecurityConfig 中来进行定义(这里需要对用户名和密码校验)。
UserDetailsService:
认证成功和认证失败的处理器:
认证成功生成 token 并返回:
认证:
拿到我的用户名,看是否有这个用户,并返回继承了 UserDetails 格式的 User(此时还是未认证):
返回 jwtUser:
拿到密码对密码进行校验:
加入认证通过,调用我的 successHandiler 处理类:
并将已认证对象 Authentication(这个类有用户名,权限等信息)放入到我的 SecurityContextHolder 上下文中。这样我们就可以在需要用到的时候从 SecurityContextHolder 中取出这个认证对象就好了。
3.2.4 认证前先校验头部是否有 token
如果不是调用的不是 login 登录接口,我们就需要检验头部是否有 token,就是第一次登录后我们返回的认证标识。
那么如何实现呢?因为认证是经过我们的 UsernamePasswordAuthenticationFilter 拦截器,那么我们可以在这个拦截器前先添加我的自定 JWT 拦截器来校验头部是否有 token。如果有就进行认证。
自定义的 JWT 拦截器:
将拦截器添加到我的 UsernamePasswordAuthenticationFilter 拦截器以实现先校验头部是否有 token,并进行认证。
3.2.5 鉴权
认证完后就要进行鉴权了。就是判断当前用户是否有访问请求的 url 的权限。
思路:通过拦截器来判断当前的请求的 url 是否哪些角色能够访问以及当前用户是否有这些角色。如果匹配不上就鉴权失败,匹配上就鉴权成功。
鉴权我们主要借助于 Spring Security 的 FilterSecurityInterceptor 拦截器来进行。
根据资源表和资源角色表拿出访问当前 url 需要的角色:
根据用户表和角色表将当前用户拥有的角色拿出来(用户的角色我们已经在认证的时候就已经拿出来了)。
进行鉴权判断(上图中认证对象角色是我们在认证的时候已经放入的了。
这里放入权限。这里的 SecurityContexthold 是一个安全的上下文,我们将通过上面认证的 Authentication 对象会放在这里面)。
最后将我们自定义的拦截器添加到 FilterSecurityInceptor 之前就可以进行认证。
JWT 的封装:
2.1 权限和角色和用户的关系
在基于角色的 RABC 权限系统中,我们通过资源与角色的绑定,来确定哪些角色拥有哪些资源,然后在通过角色与用户的绑定来确定哪些用户是什么角色,进而确定用户有哪些资源的访问权限。
所以我们在设计权限系统的表的时候就需要创建用户表、角色表、用户角色表、资源表、资源角色表。
用户表
角色表
资源表
用户角色表
角色资源表
在上面我用红框框出来的是所有表的一个共性,在实际项目中就可以把它封装成一个 BaseEntity 类,这个类会出现在我的项目的 core 子模块下。对应地我们在执行 Dao 的插入、删除操作的时候,也是需要将对共同部分的操作封装成一个基础操作,然后我们在编写 dao、service、controller 的时候,对应的继承才是最正确的。