Commit 95dcd379523359f710f5f816afc21944d166f11d

Authored by zichun
1 parent 6c97d7ef

feat(usr): LoginContext ThreadLocal REQ-USR-002

backend/src/main/java/com/xly/erp/common/security/LoginContext.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +/**
  4 + * 请求级登录上下文 — JwtHandlerInterceptor 在 preHandle 时 set,afterCompletion 时 clear。
  5 + * 用普通 ThreadLocal(不用 InheritableThreadLocal)避免子线程意外继承。
  6 + */
  7 +public final class LoginContext {
  8 +
  9 + private static final ThreadLocal<LoginUser> HOLDER = new ThreadLocal<>();
  10 +
  11 + private LoginContext() {}
  12 +
  13 + public static void set(LoginUser user) {
  14 + HOLDER.set(user);
  15 + }
  16 +
  17 + public static LoginUser current() {
  18 + return HOLDER.get();
  19 + }
  20 +
  21 + public static void clear() {
  22 + HOLDER.remove();
  23 + }
  24 +
  25 + /** 当前登录用户上下文。userType 取值 NORMAL / SUPER_ADMIN。 */
  26 + public record LoginUser(Integer userId, String username, String userType, String companyCode) {}
  27 +}
... ...
backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +import org.junit.jupiter.api.AfterEach;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import java.util.concurrent.CountDownLatch;
  7 +import java.util.concurrent.atomic.AtomicReference;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.*;
  10 +
  11 +class LoginContextTest {
  12 +
  13 + @AfterEach
  14 + void tearDown() {
  15 + LoginContext.clear();
  16 + }
  17 +
  18 + @Test
  19 + void setAndCurrent_returnsSameUser() {
  20 + LoginContext.LoginUser u = new LoginContext.LoginUser(42, "alice", "NORMAL", "HQ");
  21 + LoginContext.set(u);
  22 + assertSame(u, LoginContext.current());
  23 + }
  24 +
  25 + @Test
  26 + void clear_returnsNullForCurrent() {
  27 + LoginContext.set(new LoginContext.LoginUser(1, "x", "NORMAL", "HQ"));
  28 + LoginContext.clear();
  29 + assertNull(LoginContext.current());
  30 + }
  31 +
  32 + @Test
  33 + void setAndCurrent_isolatedPerThread() throws InterruptedException {
  34 + LoginContext.set(new LoginContext.LoginUser(1, "main", "NORMAL", "HQ"));
  35 +
  36 + AtomicReference<LoginContext.LoginUser> seen = new AtomicReference<>();
  37 + CountDownLatch latch = new CountDownLatch(1);
  38 + Thread t = new Thread(() -> {
  39 + seen.set(LoginContext.current());
  40 + LoginContext.set(new LoginContext.LoginUser(2, "child", "SUPER_ADMIN", "HQ"));
  41 + latch.countDown();
  42 + });
  43 + t.start();
  44 + latch.await();
  45 + t.join();
  46 +
  47 + assertNull(seen.get(), "子线程不应继承父线程 ThreadLocal");
  48 + assertEquals("main", LoginContext.current().username(),
  49 + "父线程上下文不应被子线程改动影响");
  50 + }
  51 +}
... ...