在SpringBoot MVC 拦截器中,曾讲过过滤器与拦截器的区别,Spring Security就是一个利用过滤器实现拦截的复杂框架。
客户端发送一个请求后,Spring Security会在进入DispatcherServlet类前添加很多的过滤器,获取用户信息、登录验证、权限验证等都可以在这些过滤器里完成。
获取过滤器:FilterChainProxy
FilterChainProxy->getFilters():
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
filterChains值如下,都是在SecurityConfig类配置的,/hello/test没有过滤器,像是不需要登录的接口都可以这样配置。
获取凭证:SecurityContextPersistenceFilter
SecurityContextPersistenceFilter->doFilter():
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
HttpSessionSecurityContextRepository->loadContext():
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
......
凭证是根据session获取到的,这也就是为什么一段时间内,一次验证通过,后续就不需要再验证的原因了。
匿名凭证:AnonymousAuthenticationFilter
AnonymousAuthenticationFilter->doFilter():
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
如果没有在session中获取到凭证,那么认为是匿名访问的。
授权、投票:FilterSecurityInterceptor
接下来执行SecurityFilter中自定义的逻辑,一切顺利的话,进入FilterSecurityInterceptor做安全检查
FilterSecurityInterceptor->invoke():
InterceptorStatusToken token = super.beforeInvocation(fi);
AbstractSecurityInterceptor->beforeInvocation():
this.accessDecisionManager.decide(authenticated, object, attributes);
这里的accessDecisionManager即为授权器,默认是AffirmativeBased,不同授权器的规则不一样,AffirmativeBased的规则是:只要有一个投同意,就通过;如果有反对票,就拒绝授权;如果设置不允许弃权,也不通过。详细代码可以看它的decide()方法。
投票器默认是WebExpressionVoter,如果是匿名凭证的情况下,它会投反对票。
当这个filter通过后,后面就进入MVC流程了。
存储凭证:SecurityContextPersistenceFilter
还是在这个过滤器,在它的finally方法中,不论执行成功或失败,都会将这一次凭证的值存入到session中,这也就是为什么下一次操作可以从session中获取凭证的原因了
SecurityContextPersistenceFilter->doFilter():
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// 当前线程清除凭证相关信息
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
异常处理:ExceptionTranslationFilter
在用户自定义过滤器中,如果不满足相关的设定(如权限不足、登录次数上限等),可以抛出异常,都会在这个过滤器捕捉到
ExceptionTranslationFilter->doFilter():
catch (Exception ex) {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
// 是否是AuthenticationException相关的异常
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
// 是否是AccessDeniedException相关的异常
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 处理security异常
handleSpringSecurityException(request, response, chain, ase);
}
else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
根据抛出的异常调用不同的处理方法,通过SecurityConfig类配置完成,如果抛出的是AccessDeniedException相关的异常,调用accessDeniedHandler(一般返回403);如果抛出的是AuthenticationException相关的异常,调用authenticationEntryPoint方法(一般返回401)