|
@@ -2,36 +2,34 @@ package com.ruoyi.system.utils;
|
|
|
|
|
|
|
|
import com.auth0.jwt.JWT;
|
|
import com.auth0.jwt.JWT;
|
|
|
import com.auth0.jwt.algorithms.Algorithm;
|
|
import com.auth0.jwt.algorithms.Algorithm;
|
|
|
|
|
+import com.ruoyi.common.constant.CacheConstants;
|
|
|
|
|
+import com.ruoyi.common.core.domain.AjaxResult;
|
|
|
import com.ruoyi.common.core.redis.RedisCache;
|
|
import com.ruoyi.common.core.redis.RedisCache;
|
|
|
-import com.ruoyi.common.exception.ServiceException;
|
|
|
|
|
-import com.ruoyi.common.utils.MessageUtils;
|
|
|
|
|
-import com.ruoyi.system.domain.InfoUser;
|
|
|
|
|
-import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
import org.aspectj.lang.ProceedingJoinPoint;
|
|
import org.aspectj.lang.ProceedingJoinPoint;
|
|
|
import org.aspectj.lang.reflect.MethodSignature;
|
|
import org.aspectj.lang.reflect.MethodSignature;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.Test;
|
|
|
-import org.mockito.MockedStatic;
|
|
|
|
|
|
|
|
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Method;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
-import java.util.Map;
|
|
|
|
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.mock;
|
|
|
-import static org.mockito.Mockito.mockStatic;
|
|
|
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.never;
|
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.verify;
|
|
|
import static org.mockito.Mockito.when;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 单设备登录(2026-09-22)会话校验切面测试:
|
|
|
|
|
+ * 新格式会话 token(jti 含冒号)必须存在 Redis 会话,被顶号/登出删除后即 401;
|
|
|
|
|
+ * 历史无冒号 jti(旧无会话重载签发)放行至自然过期;无 jti 的手工 token 同样按历史处理。
|
|
|
|
|
+ */
|
|
|
class AuthAspectTest {
|
|
class AuthAspectTest {
|
|
|
|
|
|
|
|
private RedisCache redisCache;
|
|
private RedisCache redisCache;
|
|
|
- private InfoUserMapper infoUserMapper;
|
|
|
|
|
private HttpServletRequest request;
|
|
private HttpServletRequest request;
|
|
|
private ProceedingJoinPoint joinPoint;
|
|
private ProceedingJoinPoint joinPoint;
|
|
|
private MethodSignature signature;
|
|
private MethodSignature signature;
|
|
@@ -40,36 +38,41 @@ class AuthAspectTest {
|
|
|
@BeforeEach
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
void setUp() {
|
|
|
redisCache = mock(RedisCache.class);
|
|
redisCache = mock(RedisCache.class);
|
|
|
- infoUserMapper = mock(InfoUserMapper.class);
|
|
|
|
|
request = mock(HttpServletRequest.class);
|
|
request = mock(HttpServletRequest.class);
|
|
|
joinPoint = mock(ProceedingJoinPoint.class);
|
|
joinPoint = mock(ProceedingJoinPoint.class);
|
|
|
signature = mock(MethodSignature.class);
|
|
signature = mock(MethodSignature.class);
|
|
|
- aspect = new AuthAspect(redisCache, infoUserMapper);
|
|
|
|
|
|
|
+ aspect = new AuthAspect(redisCache);
|
|
|
aspect.setHttpServletRequest(request);
|
|
aspect.setHttpServletRequest(request);
|
|
|
when(joinPoint.getSignature()).thenReturn(signature);
|
|
when(joinPoint.getSignature()).thenReturn(signature);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 手工签发新格式会话 token:jti 含冒号但不落 Redis,会话存在性由 redisCache 桩控制 */
|
|
|
|
|
+ private String colonJtiToken(String jti) {
|
|
|
|
|
+ return JWT.create()
|
|
|
|
|
+ .withClaim("id", "936")
|
|
|
|
|
+ .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
|
|
|
|
|
+ .withJWTId(jti)
|
|
|
|
|
+ .sign(Algorithm.HMAC256("TEST-AUTH-TOKEN"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Test
|
|
@Test
|
|
|
- void jwtOnlyAuthDoesNotRequireRedisSession() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "merchant");
|
|
|
|
|
- Object expected = new Object();
|
|
|
|
|
- prepareInvocation("jwtOnly", token);
|
|
|
|
|
- when(joinPoint.proceed()).thenReturn(expected);
|
|
|
|
|
|
|
+ void deletedSessionIsRejectedBeforeController() throws Throwable {
|
|
|
|
|
+ String jti = CacheConstants.USER_TOKEN_KEY + "936:deleted-session";
|
|
|
|
|
+ prepareInvocation("endpoint", colonJtiToken(jti));
|
|
|
|
|
+ when(redisCache.hasKey(jti)).thenReturn(false);
|
|
|
|
|
|
|
|
- Object actual = aspect.around(joinPoint);
|
|
|
|
|
|
|
+ Object result = aspect.around(joinPoint);
|
|
|
|
|
|
|
|
- assertSame(expected, actual);
|
|
|
|
|
- verify(redisCache, never()).hasKey(anyString());
|
|
|
|
|
- verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
|
|
|
|
|
|
|
+ assertEquals(401, ((AjaxResult) result).get("code"), "会话被删(被顶号/登出)应返回 401");
|
|
|
|
|
+ verify(joinPoint, never()).proceed();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
|
- void merchantSessionAuthAllowsExistingRedisSession() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "merchant");
|
|
|
|
|
- String jti = tokenJti(token);
|
|
|
|
|
- Object expected = new Object();
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
|
|
+ void liveSessionProceedsAndExposesTrustedIdentity() throws Throwable {
|
|
|
|
|
+ String jti = CacheConstants.USER_TOKEN_KEY + "936:live-session";
|
|
|
|
|
+ prepareInvocation("endpoint", colonJtiToken(jti));
|
|
|
when(redisCache.hasKey(jti)).thenReturn(true);
|
|
when(redisCache.hasKey(jti)).thenReturn(true);
|
|
|
|
|
+ Object expected = new Object();
|
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
|
|
|
|
|
|
Object actual = aspect.around(joinPoint);
|
|
Object actual = aspect.around(joinPoint);
|
|
@@ -80,95 +83,44 @@ class AuthAspectTest {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
|
- void merchantSessionAuthAllowsOrdinaryUserWithoutRedisSession() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "user");
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
- loginUser("0");
|
|
|
|
|
- when(redisCache.hasKey(anyString())).thenReturn(false);
|
|
|
|
|
|
|
+ void legacyNoColonJtiPassesWithoutRedisLookup() throws Throwable {
|
|
|
|
|
+ // 历史无会话重载 JwtUtil.token(id, userName) 签发:jti 为纯 UUID,不含冒号
|
|
|
|
|
+ prepareInvocation("endpoint", JwtUtil.token("936", "user"));
|
|
|
Object expected = new Object();
|
|
Object expected = new Object();
|
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
|
|
|
|
|
|
Object actual = aspect.around(joinPoint);
|
|
Object actual = aspect.around(joinPoint);
|
|
|
|
|
|
|
|
assertSame(expected, actual);
|
|
assertSame(expected, actual);
|
|
|
|
|
+ verify(redisCache, never()).hasKey(anyString());
|
|
|
verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
|
|
verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
|
- void merchantSessionAuthAllowsRiderWithoutRedisSession() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "rider");
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
- loginUser("2");
|
|
|
|
|
- when(redisCache.hasKey(anyString())).thenReturn(false);
|
|
|
|
|
|
|
+ void tokenWithoutJtiTreatedAsLegacyAndProceeds() throws Throwable {
|
|
|
|
|
+ String token = JWT.create()
|
|
|
|
|
+ .withClaim("id", "936")
|
|
|
|
|
+ .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
|
|
|
|
|
+ .sign(Algorithm.HMAC256("TEST-AUTH-TOKEN"));
|
|
|
|
|
+ prepareInvocation("endpoint", token);
|
|
|
Object expected = new Object();
|
|
Object expected = new Object();
|
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
when(joinPoint.proceed()).thenReturn(expected);
|
|
|
|
|
|
|
|
Object actual = aspect.around(joinPoint);
|
|
Object actual = aspect.around(joinPoint);
|
|
|
|
|
|
|
|
assertSame(expected, actual);
|
|
assertSame(expected, actual);
|
|
|
- verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Test
|
|
|
|
|
- void merchantSessionAuthRejectsMissingRedisSessionBeforeController() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "merchant");
|
|
|
|
|
- String jti = tokenJti(token);
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
- loginUser("1");
|
|
|
|
|
- when(redisCache.hasKey(jti)).thenReturn(false);
|
|
|
|
|
-
|
|
|
|
|
- try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
|
|
|
|
|
- messages.when(() -> MessageUtils.message("merchant.session.invalid"))
|
|
|
|
|
- .thenReturn("session invalid");
|
|
|
|
|
-
|
|
|
|
|
- ServiceException exception = assertThrows(ServiceException.class,
|
|
|
|
|
- () -> aspect.around(joinPoint));
|
|
|
|
|
-
|
|
|
|
|
- assertEquals("session invalid", exception.getMessage());
|
|
|
|
|
- verify(joinPoint, never()).proceed();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ verify(redisCache, never()).hasKey(anyString());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
|
- void merchantSessionAuthRejectsUnknownUserWithoutRedisSession() throws Throwable {
|
|
|
|
|
- String token = JwtUtil.token("936", "stranger");
|
|
|
|
|
- String jti = tokenJti(token);
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
- when(infoUserMapper.selectById(936L)).thenReturn(null);
|
|
|
|
|
- when(redisCache.hasKey(jti)).thenReturn(false);
|
|
|
|
|
-
|
|
|
|
|
- try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
|
|
|
|
|
- messages.when(() -> MessageUtils.message("merchant.session.invalid"))
|
|
|
|
|
- .thenReturn("session invalid");
|
|
|
|
|
|
|
+ void invalidSignatureRejectedBeforeSessionLookup() throws Throwable {
|
|
|
|
|
+ prepareInvocation("endpoint", "not-a-jwt");
|
|
|
|
|
|
|
|
- ServiceException exception = assertThrows(ServiceException.class,
|
|
|
|
|
- () -> aspect.around(joinPoint));
|
|
|
|
|
|
|
+ Object result = aspect.around(joinPoint);
|
|
|
|
|
|
|
|
- assertEquals("session invalid", exception.getMessage());
|
|
|
|
|
- verify(joinPoint, never()).proceed();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Test
|
|
|
|
|
- void merchantSessionAuthRejectsTokenWithoutJti() throws Throwable {
|
|
|
|
|
- String token = JWT.create()
|
|
|
|
|
- .withClaim("id", "936")
|
|
|
|
|
- .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
|
|
|
|
|
- .sign(Algorithm.HMAC256("TEST-AUTH-TOKEN"));
|
|
|
|
|
- prepareInvocation("merchantSession", token);
|
|
|
|
|
- loginUser("1");
|
|
|
|
|
-
|
|
|
|
|
- try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
|
|
|
|
|
- messages.when(() -> MessageUtils.message("merchant.session.invalid"))
|
|
|
|
|
- .thenReturn("session invalid");
|
|
|
|
|
-
|
|
|
|
|
- ServiceException exception = assertThrows(ServiceException.class,
|
|
|
|
|
- () -> aspect.around(joinPoint));
|
|
|
|
|
-
|
|
|
|
|
- assertEquals("session invalid", exception.getMessage());
|
|
|
|
|
- verify(redisCache, never()).hasKey(anyString());
|
|
|
|
|
- verify(joinPoint, never()).proceed();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ assertEquals(401, ((AjaxResult) result).get("code"));
|
|
|
|
|
+ verify(redisCache, never()).hasKey(anyString());
|
|
|
|
|
+ verify(joinPoint, never()).proceed();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void prepareInvocation(String methodName, String token) throws Exception {
|
|
private void prepareInvocation(String methodName, String token) throws Exception {
|
|
@@ -177,24 +129,9 @@ class AuthAspectTest {
|
|
|
when(request.getHeader("token")).thenReturn(token);
|
|
when(request.getHeader("token")).thenReturn(token);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void loginUser(String userType) {
|
|
|
|
|
- InfoUser user = new InfoUser();
|
|
|
|
|
- user.setUserType(userType);
|
|
|
|
|
- when(infoUserMapper.selectById(936L)).thenReturn(user);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private String tokenJti(String token) {
|
|
|
|
|
- Map<String, Object> claims = JwtUtil.verifyToken(token);
|
|
|
|
|
- return (String) claims.get("jti");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
static class SecuredMethods {
|
|
static class SecuredMethods {
|
|
|
@Auth
|
|
@Auth
|
|
|
- public void jwtOnly() {
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Auth(session = true)
|
|
|
|
|
- public void merchantSession() {
|
|
|
|
|
|
|
+ public void endpoint() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|