security的一个过滤器——SecurityContextPersistenceFilter

本文深入探讨了Spring Security中用户信息的存储与获取机制,重点介绍了SecurityContextPersistenceFilter的作用及其实现原理,解释了如何在不同请求间保持用户信息一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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配置类里的两个方法:

1public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/xxx","/js/xxx.html");
    }

2protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/xxx").permitAll()
        .anyRequest().authenticated()
    }

这两个放行的策略区别在于,第一种方式直接没有走security的过滤器,而第二种是经过security过滤器,在过滤器中判断放行

那么有以下结论:

  1. 一般web项目的登陆url放行肯定要走第二种,不经过security过滤器用户信息不会存储到session,后面的请求也无法通过上下文对象获取当前用户信息
  2. 前端静态资源的放行不涉及到用户信息,没必要走security,建议走第一种方式,此为认证,资源无权限特殊情况除外
  3. 额外放行不需要认证的的url,如果采取了第一种方式,那么在当前请求中是无法获取用户信息的,原因就是没有走我们的主角过滤器:SecurityContextPersistenceFilter,要是项目特殊,需要在放行的url里面也获取到用户信息的话,那么可以走第二种配置,有些旧的xml配置(security在mvc项目中未火的主要原因就是xml配置太过繁琐,直至springboot的javaconfig以及自动装配才大放异彩)不支持或是不方便修改,可以手动加一个过滤器模仿SecurityContextPersistenceFilter,将是放行的url提前从session中通过上下文对象SecurityContext的key值SPRING_SECURITY_CONTEXT重新获取下,手动放置SecurityContextHolder中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值