security的一个过滤器——SecurityContextPersistenceFilter
1、关于security的用户信息获取
a、SecurityContextHolder.getContext().getAuthentication()
b、在 Controller 的方法中,加入 Authentication 参数,调用getPrincipal()方法
问题:异常情况上述两种方式获取用户都为null,即系统获取不到用户信息,报错状态码401,这又是怎么回事呢?
要弄明白这个问题,我们就得明白 Spring Security 中的用户信息到底是在哪里存的?
通过第一种方式security的上下文SecurityContextHolder 中的用户数据,本质上是保存在 ThreadLocal 中,ThreadLocal 的特点是存在它里边的数据,哪个线程存的,哪个线程才能访问到。
这样就带来一个问题,当不同的请求进入到服务端之后,由不同的 thread 去处理,按理说后面的请求就可能无法获取到登录请求的线程存入的数据,例如登录请求在线程 A 中将登录用户信息存入 ThreadLocal,后面的请求来了,在线程 B 中处理,那此时就无法获取到用户的登录信息。
但实际上,正常情况下,我们每次都能够获取到登录用户信息,这又是怎么回事呢?
这我们就要引入 Spring Security 中的 SecurityContextPersistenceFilter 了。security的一系列过滤器链在经过AuthenticationFilter 之前都会先经过 SecurityContextPersistenceFilter这个过滤器。我们来看下源码:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null)
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//利用HttpSecurityContextRepository从HttpSesion中获取SecurityContext对象
//如果没有HttpSession,即浏览器第一次访问服务器,还没有产生会话。
//它会创建一个空的SecurityContext对象
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//把SecurityContext放入到SecurityContextHolder中
SecurityContextHolder.setContext(contextBeforeChainExecution);
//执行拦截链,这个链会逐层向下执行
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//当拦截器都执行完的时候把当前线程对应的SecurityContext从SecurityContextHolder中取出来
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
SecurityContextHolder.clearContext();
//利用HttpSecurityContextRepository把SecurityContext写入HttpSession
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
- doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是SecurityContextRepository的实现类HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到readSecurityContextFromSession 方法中,
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
boolean debug = this.logger.isDebugEnabled();
if (httpSession == null) {
if (debug) {
this.logger.debug("No HttpSession currently exists");
}
return null;
} else {
//此处 this.springSecurityContextKey的值就是SPRING_SECURITY_CONTEXT
Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
if (contextFromSession == null) {
if (debug) {
this.logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
}
return null;
} else if (!(contextFromSession instanceof SecurityContext)) {
if (this.logger.isWarnEnabled()) {
this.logger.warn(this.springSecurityContextKey + " did not contain a SecurityContext but contained: '" + contextFromSession + "'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?");
}
return null;
} else {
if (debug) {
this.logger.debug("Obtained a valid SecurityContext from " + this.springSecurityContextKey + ": '" + contextFromSession + "'");
}
return (SecurityContext)contextFromSession;
}
}
}
在这里我们看到了读取的核心方法 Object
contextFromSession =httpSession.getAttribute(springSecurityContextKey);,这里的
springSecurityContext对象的Key值就是SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。
-
SecurityContext 是一个接口,它有一个唯一的实现类SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。
-
在拿到SecurityContext 之后,通过 SecurityContextHolder.setContext 方法将这个
SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security
的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。 -
接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。
-
在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从
SecurityContextHolder 中获取到 SecurityContext,获取到之后,会把
SecurityContextHolder 清空,然后调用 repo.saveContext 方法将获取到的
SecurityContext 存入 session 中。
每一个请求到达服务端的时候,首先从 session 中找出来 SecurityContext ,然后设置到 SecurityContextHolder 中去,方便后续使用,当这个请求离开的时候,SecurityContextHolder 会被清空(此处操作很重要,也是security过滤器链控制安全的一种处理技巧)
,SecurityContext 会被放回 session 中,方便下一个请求来的时候获取。
那么回到最初的问题,什么时候有获取不到当前用户信息:
1、不经过security过滤器,一个新的线程中去执行SecurityContextHolder.getContext().getAuthentication()
2、SecurityContextPersistenceFilter中的session没有用户信息,如果排除第一种方式,按照该过滤器流程,就是上一个请求结束后,没有将上下文放回到session即finally里面的saveContext方法未执行
那么什么时候会出现第二种情况,就要说到security的放行url机制了,一般在WebSecurityConfigurerAdapter配置类里的两个方法:
1、public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/xxx","/js/xxx.html");
}
2、protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/xxx").permitAll()
.anyRequest().authenticated()
}
这两个放行的策略区别在于,第一种方式直接没有走security的过滤器,而第二种是经过security过滤器,在过滤器中判断放行
那么有以下结论:
- 一般web项目的登陆url放行肯定要走第二种,不经过security过滤器用户信息不会存储到session,后面的请求也无法通过上下文对象获取当前用户信息
- 前端静态资源的放行不涉及到用户信息,没必要走security,建议走第一种方式,此为认证,资源无权限特殊情况除外
- 额外放行不需要认证的的url,如果采取了第一种方式,那么在当前请求中是无法获取用户信息的,原因就是没有走我们的主角过滤器:
SecurityContextPersistenceFilter
,要是项目特殊,需要在放行的url里面也获取到用户信息的话,那么可以走第二种配置,有些旧的xml配置(security在mvc项目中未火的主要原因就是xml配置太过繁琐,直至springboot的javaconfig以及自动装配才大放异彩)不支持或是不方便修改,可以手动加一个过滤器模仿SecurityContextPersistenceFilter,将是放行的url提前从session中通过上下文对象SecurityContext的key值SPRING_SECURITY_CONTEXT重新获取下,手动放置SecurityContextHolder中