web如何取得用户信息

Spring Security 中获取用户信息的原理与实践

在 Web 应用中,Spring Security 能够在每次请求时识别用户身份并验证权限,核心在于 SecurityContextPersistenceFilterSecurityContext 的管理。本文结合源码解析这一机制的实现原理,并介绍在实际开发中获取用户信息的常用方式。

SecurityContext 的生命周期管理

SecurityContext 是存储用户认证信息的容器,其生命周期由 SecurityContextPersistenceFilter 全程管理,确保每个请求都能获取到当前用户的身份信息。

1. 核心流程:请求到来时加载上下文

当一个请求进入应用时,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;
   }
   request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

   HttpRequest// 1. 从 Session 中加载 SecurityContext
   HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
   SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

   try {
      // 2. 将上下文存入 SecurityContextHolder(ThreadLocal 存储)
      SecurityContextHolder.setContext(contextBeforeChainExecution);
      // 3. 执行后续过滤器链(包括权限校验等)
      chain.doFilter(holder.getRequest(), holder.getResponse());
   } finally {
      // 4. 请求结束:清除 ThreadLocal 中的上下文
      SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
      SecurityContextHolder.clearContext();
      // 5. 将上下文保存回 Session
      repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
      request.removeAttribute(FILTER_APPLIED);
   }
}

关键作用

  • 通过 ThreadLocal 让当前请求的所有组件(过滤器、控制器等)都能便捷访问用户信息;
  • 请求结束后清除 ThreadLocal 内容,避免线程复用导致的信息泄露。

2. 从 Session 加载 SecurityContext

loadContext 方法由 HttpSessionSecurityContextRepository 实现,负责从 Session 中读取或创建 SecurityContext

public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
   HttpServletRequest request = requestResponseHolder.getRequest();
   HttpServletResponse response = requestResponseHolder.getResponse();
   HttpSession httpSession = request.getSession(false); // 不创建新 Session

   // 1. 尝试从 Session 中读取已有上下文
   SecurityContext context = readSecurityContextFromSession(httpSession);

   // 2. 若 Session 中无上下文,创建新的空上下文
   if (context == null) {
      context = generateNewContext();
   }

   // 3. 包装响应对象,确保上下文能被正确保存
   SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
         response, request, httpSession != null, context);
   requestResponseHolder.setResponse(wrappedResponse);

   return context;
}

// 从 Session 中读取 SecurityContext
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
   if (httpSession == null) {
      return null;
   }
   // Session 中存储上下文的默认键:SPRING_SECURITY_CONTEXT
   Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
   return (contextFromSession instanceof SecurityContext) ? 
          (SecurityContext) contextFromSession : null;
}

3. 将 SecurityContext 保存回 Session

请求处理完成后,saveContext 方法将更新后的 SecurityContext 存储到 Session 中,确保用户身份信息在会话期间保持一致:

public void saveContext(SecurityContext context, HttpServletRequest request,
      HttpServletResponse response) {
   // 获取包装后的响应对象
   SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils
         .getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class);
   
   if (!responseWrapper.isContextSaved()) {
      responseWrapper.saveContext(context); // 保存上下文
   }
}

// 实际执行保存逻辑
protected void saveContext(SecurityContext context) {
   final Authentication authentication = context.getAuthentication();
   HttpSession httpSession = request.getSession(false);

   // 处理匿名用户或空认证信息:从 Session 中移除上下文
   if (authentication == null || trustResolver.isAnonymous(authentication)) {
      if (httpSession != null) {
         httpSession.removeAttribute(springSecurityContextKey);
      }
      return;
   }

   // 若 Session 不存在,按需创建新 Session
   if (httpSession == null) {
      httpSession = createNewSessionIfAllowed(context);
   }

   // 将上下文存入 Session(仅当内容变化或首次存储时)
   if (httpSession != null) {
      if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
         httpSession.setAttribute(springSecurityContextKey, context);
      }
   }
}

在 Web 应用中获取用户信息的常用方式

基于上述机制,我们可以在控制器、服务层等任意组件中获取当前登录用户的信息,核心是通过 SecurityContextHolder 访问 SecurityContext

1. 直接通过 SecurityContextHolder 获取

这是最通用的方式,适用于任何组件(控制器、服务、工具类等):

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

public class UserInfoUtil {
    /**
     * 获取当前登录用户名
     */
    public static String getCurrentUsername() {
        // 1. 获取 Authentication 对象(存储用户认证信息)
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null) {
            return null;
        }
        
        // 2. 获取用户主体(通常是 UserDetails 实例)
        Object principal = authentication.getPrincipal();
        
        if (principal instanceof UserDetails) {
            // 3. 从 UserDetails 中获取用户名
            return ((UserDetails) principal).getUsername();
        }
        
        // 处理非 UserDetails 类型的主体(如匿名用户)
        return principal.toString();
    }
    
    /**
     * 获取当前登录用户的所有权限
     */
    public static Collection<? extends GrantedAuthority> getCurrentAuthorities() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null ? authentication.getAuthorities() : Collections.emptyList();
    }
}

2. 在控制器中通过 @AuthenticationPrincipal 注解获取

Spring Security 提供了 @AuthenticationPrincipal 注解,可直接在控制器方法参数中注入当前用户的 UserDetails 或自定义用户对象:

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    
    // 注入 UserDetails 对象
    @GetMapping("/user/info")
    public String getUserInfo(@AuthenticationPrincipal UserDetails userDetails) {
        return "当前登录用户:" + userDetails.getUsername() + 
               ",权限:" + userDetails.getAuthorities();
    }
    
    // 注入自定义用户对象(需实现 UserDetails)
    @GetMapping("/user/custom")
    public String getCustomUser(@AuthenticationPrincipal CustomUser customUser) {
        return "用户ID:" + customUser.getId() + 
               ",用户名:" + customUser.getUsername();
    }
}

// 自定义用户类示例
public class CustomUser implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    
    // getter/setter 及其他实现方法...
}

3. 通过 HttpServletRequest 获取

在 Web 环境中,用户信息也会被存入 HttpServletRequest 的属性中,可通过以下方式获取:

import javax.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RequestController {
    
    @GetMapping("/user/from-request")
    public String getUserFromRequest(HttpServletRequest request) {
        // 从请求属性中获取 Authentication 对象
        UsernamePasswordAuthenticationToken auth = 
            (UsernamePasswordAuthenticationToken) request.getUserPrincipal();
        
        if (auth != null) {
            return "从请求中获取的用户:" + auth.getName();
        }
        return "未登录";
    }
}

关键对象解析

  1. SecurityContextHolder
    • 静态工具类,通过 ThreadLocal 存储 SecurityContext
    • 提供 getContext()setContext() 方法操作上下文。
  2. SecurityContext
    • 存储用户认证信息的容器,核心方法是 getAuthentication()
    • 本质是一个接口,默认实现为 SecurityContextImpl
  3. Authentication
    • 存储用户身份信息(用户名、密码、权限等);
    • 认证成功后包含 UserDetails 类型的主体(principal)。
  4. UserDetails
    • 封装用户核心信息(用户名、密码、权限、账号状态等);
    • 通常需要自定义实现以扩展用户属性(如用户 ID、昵称等)。

特殊场景处理

  1. 匿名用户

    • 未登录用户会被分配匿名身份(AnonymousAuthenticationToken);

    • 可通过Authentication的isAnonymous()方法判断:

      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      boolean isAnonymous = auth != null && auth instanceof AnonymousAuthenticationToken;
  2. 异步线程中获取用户信息

    • ThreadLocal 存储的信息无法直接传递到异步线程;

    • 解决方案:手动传递SecurityContext到子线程:

      // 主线程中获取上下文
      SecurityContext context = SecurityContextHolder.getContext();
      
      // 异步执行
      CompletableFuture.runAsync(() -> {
          try {
              // 子线程中设置上下文
              SecurityContextHolder.setContext(context);
              // 处理业务逻辑...
          } finally {
              // 清除子线程的上下文
              SecurityContextHolder.clearContext();
          }
      });

总结

Spring Security 通过 SecurityContextPersistenceFilter 实现了 SecurityContext 与 Session 的绑定,结合 ThreadLocal 确保每个请求都能便捷访问用户信息:

  1. 请求到来时,从 Session 加载 SecurityContext 并存入 SecurityContextHolder
  2. 请求处理过程中,通过 SecurityContextHolder 或注解获取用户信息;
  3. 请求结束后,将 SecurityContext 保存回 Session 并清除 ThreadLocal