一、整理了解下Spring Security 的工作原理
如上图所示,spring security 的主要工作就是在原有的网络请求的过滤器链(ApplicationFilterChain)中额外添加一条过滤器链(FilterChainProxy),主要用于用户认证与授权。
请求进入web容器经一些容器自身的基础加工后,进入到servlet的滤器链中,spring security 使用 DelegatingFilterProxy 这个filter,将 targetBeanName 设置为 "springSecurityFilterChain" ,也就是security生成的 FilterChainProxy 这个类的bean的名字,使 servlet 过滤器链 指向了 spring security的过滤器链 ,同时也进入到spring 容器当中。经过一系列的认证工作,若认证成功后,会生成一个Authentication类型的对象(可以记录当前登录人的用户信息,权限信息),放入到SecurityContext中(默认以ThreadLocal的方式保存),然后返回servlet 过滤器链中,向控制层前行。
security的过滤器链会根据你所配置需要启停的功能增减,同时你也可以创建自己的过滤器添加进这个过滤器链中。
二、FilterChainProxy源码分析
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//此处是对请求是否第一次经过这个过滤器的判断,在spring security中的过滤器中这种用法是相当常见的
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
//清除security上下文中的保存的Authentication对象
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//对request进行一层包装,同时校验了一下请求的方式是否允许,以及请求地址中是否有非法字符,若不通过抛出异常
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
//对response进行了一层包装,若在response中添加header或cookie时,会对header和cookie的一些属性进行校验
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
//获取spring security中第一个匹配请求地址的过滤器列表
List filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
//将过滤器列表和servlet过滤器链包装成一个新的过滤器链 VirtualFilterChain 是FilterChainProxy的内部类
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//security 过滤器执行结束后,继续执行servlet过滤器链
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
//传入this,nextFilter中的会继续调用VirtualFilterChain.doFilter()直至所有过滤器执行结束或抛出异常
nextFilter.doFilter(request, response, this);
}
}
}
FilterChainProxy过滤器通过内部的FilterChain,将请求引入到了security的过滤器链中,上述对其主要的方法进行了简单描述。
三、基于SpringBoot,security的加载过程
springboot 启动时 有关spring security 主要加载的类有SecurityAutoConfiguration、SecurityRequestMatcherProviderAutoConfiguration、UserDetailsServiceAutoConfiguration、SecurityFilterAutoConfiguration , 其中需要讲一下SecurityAutoConfiguration 和SecurityFilterAutoConfiguration。
1、 SecurityAutoConfiguration 引入了SpringBootWebSecurityConfiguration、WebSecurityEnablerConfiguration、SecurityDataConfiguration 这三个配置类:
SpringBootWebSecurityConfiguration 当你没有主动配置security时,这个类的作用会生效,生一套默认的配置
WebSecurityEnablerConfiguration 的作用全在@EnableWebSecurity 这个注解上,使Security功能生效,并且承担了其中最主要的初始化工作。
@EnableWebSecurity 引入了 WebSecurityConfiguration类 ,在这个类中读取了所有的security配置(可以配置多个spring security 过滤器链),并根据这些配置生成一个name为“springSecurityFilterChain” 的bean,里边进行的动作相当多,有兴趣的同学可以研读一下这一块源码。
SecurityDataConfiguration 属于spring security 的一个扩展配置,此处省略。
2、SecurityFilterAutoConfiguration 的作用只有一个,就是创建一个DelegatingFilterProxyRegistrationBean,将spring security 的过滤器链springSecurityFilterChain 与servlet过滤器相连。
四、注意事项
1、当手动添加过滤器到spring security中时,如果这个过滤器添加到spring 容器中,spring 自动会将此过滤器添加到servlet过滤器链中,此时,两条过滤器链都会有这个过滤器,则会执行两次。
解决办法:
让这个过滤器继承OncePerRequestFilter ,这个过滤器内部保证只执行一次。
或者不要将这个过滤器添加到spring容器中。
2、在使用spring security 自带的csrf防护功能时,在前端并发请求会导致认证不通过,原因是前端同时需要发起多个请求时,这几个请求的发送总是会有一些先后顺序,当第n(n>1)个请求准备发起时,获取到的cookie中的csrfToken为token1, 此时第一个请求的已响应,cookie中的csrfToken随之替换成第一个请求中返回的cookie中的csrfToken2,这时第n个请求发出,header中携带的是token1,cookie中放的是token2,两者不相等,请求拒绝。 所以这个功能要视具体情况而用。