From 95dcd379523359f710f5f816afc21944d166f11d Mon Sep 17 00:00:00 2001 From: zichun Date: Fri, 15 May 2026 09:12:19 +0800 Subject: [PATCH] feat(usr): LoginContext ThreadLocal REQ-USR-002 --- backend/src/main/java/com/xly/erp/common/security/LoginContext.java | 27 +++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/common/security/LoginContext.java create mode 100644 backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java diff --git a/backend/src/main/java/com/xly/erp/common/security/LoginContext.java b/backend/src/main/java/com/xly/erp/common/security/LoginContext.java new file mode 100644 index 0000000..916482e --- /dev/null +++ b/backend/src/main/java/com/xly/erp/common/security/LoginContext.java @@ -0,0 +1,27 @@ +package com.xly.erp.common.security; + +/** + * 请求级登录上下文 — JwtHandlerInterceptor 在 preHandle 时 set,afterCompletion 时 clear。 + * 用普通 ThreadLocal(不用 InheritableThreadLocal)避免子线程意外继承。 + */ +public final class LoginContext { + + private static final ThreadLocal HOLDER = new ThreadLocal<>(); + + private LoginContext() {} + + public static void set(LoginUser user) { + HOLDER.set(user); + } + + public static LoginUser current() { + return HOLDER.get(); + } + + public static void clear() { + HOLDER.remove(); + } + + /** 当前登录用户上下文。userType 取值 NORMAL / SUPER_ADMIN。 */ + public record LoginUser(Integer userId, String username, String userType, String companyCode) {} +} diff --git a/backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java b/backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java new file mode 100644 index 0000000..0fdd7e3 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/common/security/LoginContextTest.java @@ -0,0 +1,51 @@ +package com.xly.erp.common.security; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +class LoginContextTest { + + @AfterEach + void tearDown() { + LoginContext.clear(); + } + + @Test + void setAndCurrent_returnsSameUser() { + LoginContext.LoginUser u = new LoginContext.LoginUser(42, "alice", "NORMAL", "HQ"); + LoginContext.set(u); + assertSame(u, LoginContext.current()); + } + + @Test + void clear_returnsNullForCurrent() { + LoginContext.set(new LoginContext.LoginUser(1, "x", "NORMAL", "HQ")); + LoginContext.clear(); + assertNull(LoginContext.current()); + } + + @Test + void setAndCurrent_isolatedPerThread() throws InterruptedException { + LoginContext.set(new LoginContext.LoginUser(1, "main", "NORMAL", "HQ")); + + AtomicReference seen = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + Thread t = new Thread(() -> { + seen.set(LoginContext.current()); + LoginContext.set(new LoginContext.LoginUser(2, "child", "SUPER_ADMIN", "HQ")); + latch.countDown(); + }); + t.start(); + latch.await(); + t.join(); + + assertNull(seen.get(), "子线程不应继承父线程 ThreadLocal"); + assertEquals("main", LoginContext.current().username(), + "父线程上下文不应被子线程改动影响"); + } +} -- libgit2 0.22.2