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 | +} | ... | ... |