Spring Security 登录获取用户信息流程分析
Spring Security 登录获取用户信息方式
如果使用了 Spring Security,当我们登录成功后,可以通过如下方式获取到当前登录用户信息:
-
SecurityContextHolder.getContext().getAuthentication()
-
在 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
- 获取 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;
}
- 获取 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 中
- 保存 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);
}
- 保存 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));
}
}
}
}
详细步骤(原本的方法很长,这里列出来比较关键的几个部分):
-
SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,所以 SecurityContextPersistenceFilter 作为一个过滤器,它里边最重要的方法就是 doFilter 了。
-
在 doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession 方法中,在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 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 会被清空,SecurityContext 会被放回 session 中,方便下一个请求来的时候获取。
SecurityConfigurer 配置项
- configure(HttpSecurity http)
- configure(WebSecurity web)
configure(HttpSecurity http)
- 经过过滤器链
- 通常放行登录接口放在这里,因为要将登录信息存入 session 中
configure(WebSecurity web)
- 不经过过滤器链,不会将登录信息存入 session 中
- 通常用于放行静态资源,css、html 等