0%

web如何取得用户信息

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

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

SecurityContext 的生命周期管理

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

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

当一个请求进入应用时,SecurityContextPersistenceFilter 会执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 中,确保用户身份信息在会话期间保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 获取

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 或自定义用户对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 的属性中,可通过以下方式获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()方法判断:

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 主线程中获取上下文
      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

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10