Spring Security 登录获取用户信息流程分析

Spring Security 登录获取用户信息方式

如果使用了 Spring Security,当我们登录成功后,可以通过如下方式获取到当前登录用户信息:

  1. SecurityContextHolder.getContext().getAuthentication()

  2. 在 Controller 的方法中,加入 Authentication 参数

这两种办法,都可以获取到当前登录用户信息。

SecurityContextHolder 简介

SecurityContextHolder 中的数据,本质上是保存在 ThreadLocal 中,ThreadLocal 的特点是存在它里边的数据,哪个线程存的,哪个线程才能访问到。

流程分析

无论是 Spring Security 还是 Shiro,它的一系列功能其实都是由过滤器来完成的,在 Spring Security 中,有一些重要的过滤器比如 UsernamePasswordAuthenticationFilter 过滤器,在这个过滤器之前,还有一个过滤器就是 SecurityContextPersistenceFilter,请求在到达 UsernamePasswordAuthenticationFilter 之前都会先经过SecurityContextPersistenceFilter。

我们在成功进行登录了之后,AbstractAuthenticationProcessingFilter#successfulAuthentication 中有这么一段代码,SecurityContextHolder.getContext().setAuthentication(authResult); 会将 Authentication 设置到 SecurityContext 中,后续 SecurityContextPersistenceFilter 会将 SecurityContext 保存到 session 中。

SecurityContextPersistenceFilter 核心源码

static final String FILTER_APPLIED = "__spring_security_scpf_applied";

// 两个子类,这里我们看 HttpSessionSecurityContextRepository 实现类
private SecurityContextRepository repo;

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// ensure that filter is only applied once per request
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		if (this.forceEagerSessionCreation) {
			HttpSession session = request.getSession();
			if (this.logger.isDebugEnabled() && session.isNew()) {
				this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
			}
		}
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		// 核心-从 session 中获取用户认证信息
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
			// 保存到线程上线文中[SecurityContextHolder 内部的 SecurityContextHolderStrategy 有三种实现,默认使用的是 ThreadLocalSecurityContextHolderStrategy,而 ThreadLocalSecurityContextHolderStrategy 内部就是 ThreadLocal ],所以后续就可以获取到用户认证信息了
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// Crucial removal of SecurityContextHolder contents before anything else.
			SecurityContextHolder.clearContext();
			// 重新保存到 session 中
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
			this.logger.debug("Cleared SecurityContextHolder to complete request");
		}
	}

HttpSessionSecurityContextRepository

从 session 中获取 认证信息 SecurityContext
  1. 获取 SecurityContext - 步骤1
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
    HttpServletRequest request = requestResponseHolder.getRequest();
    HttpServletResponse response = requestResponseHolder.getResponse();
    HttpSession httpSession = request.getSession(false);
    // 从 session 中获取 securityContext 实例,该接口只有一个实现就是 SecurityContextImpl,仅仅是保存和获取 Authentication 实例
    SecurityContext context = readSecurityContextFromSession(httpSession);
    if (context == null) {
        context = generateNewContext();
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Created %s", context));
        }
    }
    SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
            httpSession != null, context);
    requestResponseHolder.setResponse(wrappedResponse);
    requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
    return context;
}
  1. 获取 SecurityContext - 步骤2
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
    if (httpSession == null) {
        this.logger.trace("No HttpSession currently exists");
        return null;
    }
    // Session exists, so try to obtain a context from it.
    Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
    if (contextFromSession == null) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Did not find SecurityContext in HttpSession %s "
                    + "using the SPRING_SECURITY_CONTEXT session attribute", httpSession.getId()));
        }
        return null;
    }

    // We now have the security context object from the session.
    if (!(contextFromSession instanceof SecurityContext)) {
        this.logger.warn(LogMessage.format(
                "%s did not contain a SecurityContext but contained: '%s'; are you improperly "
                        + "modifying the HttpSession directly (you should always use SecurityContextHolder) "
                        + "or using the HttpSession attribute reserved for this class?",
                this.springSecurityContextKey, contextFromSession));
        return null;
    }

    if (this.logger.isTraceEnabled()) {
        this.logger.trace(
                LogMessage.format("Retrieved %s from %s", contextFromSession, this.springSecurityContextKey));
    }
    else if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Retrieved %s", contextFromSession));
    }
    // Everything OK. The only non-null return from this method.
    return (SecurityContext) contextFromSession;
}
将认证信息 SecurityContext 保存到 session 中
  1. 保存 SecurityContext 到 session 中 - 1
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
    SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
            SaveContextOnUpdateOrErrorResponseWrapper.class);
    Assert.state(responseWrapper != null, () -> "Cannot invoke saveContext on response " + response
            + ". You must use the HttpRequestResponseHolder.response after invoking loadContext");
    responseWrapper.saveContext(context);
}
  1. 保存 SecurityContext 到 session 中 - 2
@Override
protected void saveContext(SecurityContext context) {
    final Authentication authentication = context.getAuthentication();
    HttpSession httpSession = this.request.getSession(false);
    String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
    // See SEC-776
    if (authentication == null
            || HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
        if (httpSession != null && this.authBeforeExecution != null) {
            // SEC-1587 A non-anonymous context may still be in the session
            // SEC-1735 remove if the contextBeforeExecution was not anonymous
            httpSession.removeAttribute(springSecurityContextKey);
            this.isSaveContextInvoked = true;
        }
        if (this.logger.isDebugEnabled()) {
            if (authentication == null) {
                this.logger.debug("Did not store empty SecurityContext");
            }
            else {
                this.logger.debug("Did not store anonymous SecurityContext");
            }
        }
        return;
    }
    httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);
    // If HttpSession exists, store current SecurityContext but only if it has
    // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
    if (httpSession != null) {
        // We may have a new session, so check also whether the context attribute
        // is set SEC-1561
        if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
        	// 将 SecurityContext  保存到 session 中
            httpSession.setAttribute(springSecurityContextKey, context);
            this.isSaveContextInvoked = true;
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
            }
        }
    }
}

详细步骤(原本的方法很长,这里列出来比较关键的几个部分):

  1. SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,所以 SecurityContextPersistenceFilter 作为一个过滤器,它里边最重要的方法就是 doFilter 了。

  2. 在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession 方法中,在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。

  3. SecurityContext 是一个接口,它有一个唯一的实现类 SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。

  4. 在拿到 SecurityContext 之后,通过 SecurityContextHolder.setContext 方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。

  5. 接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。

  6. 在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder 中获取到 SecurityContext,获取到之后,会把 SecurityContextHolder 清空,然后调用 repo.saveContext 方法将获取到的 SecurityContext 存入 session 中。

每一个请求到达服务端的时候,首先从 session 中找出来 SecurityContext ,然后设置到 SecurityContextHolder 中去,方便后续使用,当这个请求离开的时候,SecurityContextHolder 会被清空,SecurityContext 会被放回 session 中,方便下一个请求来的时候获取。

SecurityConfigurer 配置项

  1. configure(HttpSecurity http)
  2. configure(WebSecurity web)

configure(HttpSecurity http)

  1. 经过过滤器链
  2. 通常放行登录接口放在这里,因为要将登录信息存入 session 中

configure(WebSecurity web)

  1. 不经过过滤器链,不会将登录信息存入 session 中
  2. 通常用于放行静态资源,css、html 等

参考

  1. Spring Security 登录成功后总是获取不到登录用户信息
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值