咱们从源码的角度来看看SpringSecurity的处理过程,整个过程比较长,设计的函数调用也比较多,为了在解释详细的同时增加咱们的可读性,我在源码的关键步骤都添加了小标题,大家可以通过小标题来了解大概过程,其他步骤仅以图片形式展示,方便大家一步步跟下来。那一起来看看吧!
这里咱们以单体架构为例,使用JWT和SpringSecurity结合之后的样例来讲解。
@Configuration
//开启注解授权认证的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 为了方便阅读,这里省略一部分非重要代码,需要全部的代码评论即可
**/
// 把我们自己写好的过滤器添加到过滤器的前面
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
首先是JwtAuthenticationTokenFilter这个过滤器,这个是咱们整合JWT进行权限验证的关键组成部分,这个过滤器完全由我们自定义,可以实现我们想要做的功能,这里提供一个样例
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
RoleandpermService roleandpermService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
// 这里放行就是让其他的过滤器帮我们解决未登录
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 解析token
String userid = "";
try {
Claims claims = JwtUtils.getClaims(token);
userid = (String) claims.get("userid");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("解析token异常");
}
// redis中获取信息
User o = (User) redisTemplate.opsForValue().get("login" + userid);
if (Objects.isNull(o)) {
throw new RuntimeException("token异常");
}
// 拿到用户的权限,并对权限进行封装
List<Roleandperm> byUserId = roleandpermService.getByUserId(new Long(o.getUser_id()));
List<GrantedAuthority> newList=new LinkedList<>();
List<String> perms=new LinkedList<>();
byUserId.forEach(p->{
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getPerm());
newList.add(simpleGrantedAuthority);
perms.add(p.getPerm());
});
// 存入SecurityContextgholder,因为后续的过滤器需要在这个东西中找到认证的信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(new LoginUser(o,perms), null, newList);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
大家看上面的注解其实能理解这个过滤器是干啥的,其实就是截取请求中的Headers中的token信息,然后对这个token进行解析,然后根据token中的id信息在Redis中拿到权限信息,并且封装成为UsernamePasswordAuthenticationToken对象,交给后续过滤器进行处理。
在过滤器中我们进行权限验证了吗?
很明显,没有的。我们只是把权限封装起来,交给后续的过滤器使用了。
那他到底是什么时候调用的呢?
开始进入源码状态了,大家准备好!
首先就是FilterChainProxy的doFilter方法,
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// 判断是否所有过滤器都处理结束了
if (this.currentPosition == this.size) {
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(LogMessage.of(() -> {
return "Secured " + FilterChainProxy.requestLine(this.firewalledRequest);
}));
}
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
} else {
// 指向过滤器的下标
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isTraceEnabled()) {
FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
}
}
上面的代码简单来说,就是循环遍历additionalFilters中的过滤器,然后依次对请求进行处理,我们看以下additionalFilters到底存了什么
你会发现我们自定义的JwtAuthenticationTokenFilter 也在里面,这说明在遍历过程中,我们自定义的Filter也会对请求进行处理。
现在咱们回到JwtAuthenticationTokenFilter中,你会发现我们把用户和权限信息放在SecurityContextHolder.getContext里面,这是干啥用的呢,咱们接着往下看。
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
前面咱们提到了在SecurityContextHolder.getContext()里把usernamePasswordAuthenticationToken放进去了,那放进去是为了干什么呢?
一个请求过来,我们是不是要判断登陆状态,那我们是不是可以基于usernamePasswordAuthenticationToken来判断是否登录呢?
答案是可以的
那他是怎么判断的呢
由于篇幅原因咱们直接来到FilterSecurityInterceptor的处理过程。
然后一步步点进去
注意这里,authenticated是不是有点熟悉,我们刚才怎么放用户信息来着,是不是
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken)
我们点进去看看,他获取了什么
是不是发现了我们的老朋友SecurityContextHolder.getContext(),通过getAuthentication就获取到了我们的用户信息和权限信息
接下来返回到AbstractSecurityInterceptor,接着往下走,一路点进去
就来到了上图的地方,这里很有意思哈。我们看一下他要做什么,SpringSecurity在判断用户状态时用的是一种叫做投票器的机制。可以理解为有一个投票通过那么该请求就可以继续执行,如果一票都没通过,那就直接抛出异常就可以了。
voter就可以认为是投票者,点进去
关键来了,兄弟们,这里的!isAnonymous是什么意思呢?
着我们就要回溯以下之前的过滤器AnonymousAuthenticationFilter
注意哈,咱们的JwtAuthenticationTokenFilter执行顺序是在这个过滤器之前的
如果说你没有传递token,那我们就不会往SecurityContextHolder.getContext()放认证对象,那么AnonymousAuthenticationFilter就会帮我们放一个Anonymous的认证对象进去,这样我们在FilterSecurityInterceptor就可以判断请求的用户是否已经登陆。
接下来一路返回到咱们投票器的部分
这里的result为1,也就直接返回,就可以进行后续的操作了,并不会抛出异常,否则会deny++。
当没有直接返回并且deny>0,就会抛出异常也就是用户授权未通过,那就直接返回403了。
到这里为止,我们实现了什么功能?
就是一个判断用户是否登录的过程,那验权是不是还没做,别着急,咱们接着看。
我们看下不传token会怎样,直接看Voter这边
你会发现deny直接++
然后接下来就开始抛出异常了
然后交给ExceptionTranslationFilter进行处理,注意哈这也是咱们之前调用的过滤器链中的一个过滤器,然后就可以返回403了。
到现在我们大概了解了SpringSecurity怎么判断请求是否登录的。简单来说就是我们可以通过自定过滤器的方式,将用户信息和权限信息封装起来存储到的SecurityContextHolder中,然后再由后续的FilterSecurityInterceptor判断是否登录,未登陆的话SecurityContextHolder存储的是匿名用户,这样就可以判断是否登录了。今天就到这了,下期咱们讲讲SpringSecurity怎么做的权限验证!