web如何取得用户信息
Spring Security 中获取用户信息的原理与实践
在 Web 应用中,Spring Security 能够在每次请求时识别用户身份并验证权限,核心在于 SecurityContextPersistenceFilter 对 SecurityContext 的管理。本文结合源码解析这一机制的实现原理,并介绍在实际开发中获取用户信息的常用方式。
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;
public class UserController {
// 注入 UserDetails 对象
public String getUserInfo( UserDetails userDetails) {
return "当前登录用户:" + userDetails.getUsername() +
",权限:" + userDetails.getAuthorities();
}
// 注入自定义用户对象(需实现 UserDetails)
public String getCustomUser( 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;
public class RequestController {
public String getUserFromRequest(HttpServletRequest request) {
// 从请求属性中获取 Authentication 对象
UsernamePasswordAuthenticationToken auth =
(UsernamePasswordAuthenticationToken) request.getUserPrincipal();
if (auth != null) {
return "从请求中获取的用户:" + auth.getName();
}
return "未登录";
}
}
关键对象解析
- SecurityContextHolder:
- 静态工具类,通过
ThreadLocal存储SecurityContext; - 提供
getContext()和setContext()方法操作上下文。
- 静态工具类,通过
- SecurityContext:
- 存储用户认证信息的容器,核心方法是
getAuthentication(); - 本质是一个接口,默认实现为
SecurityContextImpl。
- 存储用户认证信息的容器,核心方法是
- Authentication:
- 存储用户身份信息(用户名、密码、权限等);
- 认证成功后包含
UserDetails类型的主体(principal)。
- UserDetails:
- 封装用户核心信息(用户名、密码、权限、账号状态等);
- 通常需要自定义实现以扩展用户属性(如用户 ID、昵称等)。
特殊场景处理
匿名用户:
未登录用户会被分配匿名身份(
AnonymousAuthenticationToken);可通过Authentication的isAnonymous()方法判断:
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); boolean isAnonymous = auth != null && auth instanceof AnonymousAuthenticationToken;
异步线程中获取用户信息:
ThreadLocal存储的信息无法直接传递到异步线程;解决方案:手动传递SecurityContext到子线程:
// 主线程中获取上下文 SecurityContext context = SecurityContextHolder.getContext(); // 异步执行 CompletableFuture.runAsync(() -> { try { // 子线程中设置上下文 SecurityContextHolder.setContext(context); // 处理业务逻辑... } finally { // 清除子线程的上下文 SecurityContextHolder.clearContext(); } });
总结
Spring Security 通过 SecurityContextPersistenceFilter 实现了 SecurityContext 与 Session 的绑定,结合 ThreadLocal 确保每个请求都能便捷访问用户信息:
- 请求到来时,从 Session 加载
SecurityContext并存入SecurityContextHolder; - 请求处理过程中,通过
SecurityContextHolder或注解获取用户信息; - 请求结束后,将
SecurityContext保存回 Session 并清除ThreadLocal