Commit 95dcd379523359f710f5f816afc21944d166f11d
1 parent
6c97d7ef
feat(usr): LoginContext ThreadLocal REQ-USR-002
Showing
2 changed files
with
78 additions
and
0 deletions
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 | +} |