去掉shiro框架
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
package com.label.integration;
|
||||
|
||||
import com.label.AbstractIntegrationTest;
|
||||
import com.label.common.result.Result;
|
||||
import com.label.service.RedisService;
|
||||
import com.label.util.RedisUtil;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Shiro 过滤器集成测试:
|
||||
* - 无 Token → 401 Unauthorized
|
||||
* - Token 不存在(已过期或伪造)→ 401 Unauthorized
|
||||
* - 有效 Token 但角色不足(ANNOTATOR 访问 REVIEWER 端点)→ 403 Forbidden
|
||||
* - 有效 Token 且角色满足(REVIEWER 访问 REVIEWER 端点)→ 200 OK
|
||||
*/
|
||||
@Import(ShiroFilterIntegrationTest.TestConfig.class)
|
||||
public class ShiroFilterIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
/** 仅供测试的临时 Token,测试结束后清理 */
|
||||
private static final String REVIEWER_TOKEN = "test-reviewer-token-uuid-fixed";
|
||||
private static final String ANNOTATOR_TOKEN = "test-annotator-token-uuid-fixed";
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
// ------------------------------------------------------------------ 测试 Controller --
|
||||
|
||||
/**
|
||||
* 测试专用配置:注册仅在测试环境存在的端点
|
||||
*/
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
public ReviewerOnlyController reviewerOnlyController() {
|
||||
return new ReviewerOnlyController();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要 REVIEWER 角色的测试端点。
|
||||
* 调用 subject.checkRole() —— 角色不足时抛出 AuthorizationException → 403。
|
||||
*/
|
||||
@RestController
|
||||
static class ReviewerOnlyController {
|
||||
@GetMapping("/api/test/reviewer-only")
|
||||
public Result<String> reviewerOnly() {
|
||||
// 验证当前 Subject 是否持有 REVIEWER 角色
|
||||
SecurityUtils.getSubject().checkRole("REVIEWER");
|
||||
return Result.success("ok");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 测试前后置 --
|
||||
|
||||
@BeforeEach
|
||||
void setupTokens() {
|
||||
// REVIEWER Token:companyId=1, userId=2
|
||||
Map<String, String> reviewerData = new HashMap<>();
|
||||
reviewerData.put("userId", "2");
|
||||
reviewerData.put("role", "REVIEWER");
|
||||
reviewerData.put("companyId", "1");
|
||||
reviewerData.put("username", "reviewer01");
|
||||
redisService.hSetAll(RedisUtil.tokenKey(REVIEWER_TOKEN), reviewerData, 3600L);
|
||||
|
||||
// ANNOTATOR Token:companyId=1, userId=3
|
||||
Map<String, String> annotatorData = new HashMap<>();
|
||||
annotatorData.put("userId", "3");
|
||||
annotatorData.put("role", "ANNOTATOR");
|
||||
annotatorData.put("companyId", "1");
|
||||
annotatorData.put("username", "annotator01");
|
||||
redisService.hSetAll(RedisUtil.tokenKey(ANNOTATOR_TOKEN), annotatorData, 3600L);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanupTokens() {
|
||||
redisService.delete(RedisUtil.tokenKey(REVIEWER_TOKEN));
|
||||
redisService.delete(RedisUtil.tokenKey(ANNOTATOR_TOKEN));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 测试用例 --
|
||||
|
||||
@Test
|
||||
@DisplayName("无 Authorization 头 → 401 Unauthorized")
|
||||
void noToken_returns401() {
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(
|
||||
baseUrl("/api/test/reviewer-only"), String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Token 不存在于 Redis → 401 Unauthorized")
|
||||
void expiredToken_returns401() {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
baseUrl("/api/test/reviewer-only"),
|
||||
HttpMethod.GET,
|
||||
bearerRequest("non-existent-token-xyz"),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有效 Token 但角色不足(ANNOTATOR 访问 REVIEWER 端点)→ 403 Forbidden")
|
||||
void annotatorToken_onReviewerEndpoint_returns403() {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
baseUrl("/api/test/reviewer-only"),
|
||||
HttpMethod.GET,
|
||||
bearerRequest(ANNOTATOR_TOKEN),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有效 Token 且角色满足(REVIEWER 访问 REVIEWER 端点)→ 200 OK")
|
||||
void reviewerToken_onReviewerEndpoint_returns200() {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
baseUrl("/api/test/reviewer-only"),
|
||||
HttpMethod.GET,
|
||||
bearerRequest(REVIEWER_TOKEN),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 工具方法 --
|
||||
|
||||
/** 构造带 Bearer Token 的请求实体 */
|
||||
private HttpEntity<Void> bearerRequest(String token) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + token);
|
||||
return new HttpEntity<>(headers);
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class ApplicationConfigTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("application.yml 提供 Swagger 和 shiro.auth 测试开关配置")
|
||||
void applicationYaml_containsSwaggerAndShiroAuthToggle() throws Exception {
|
||||
@DisplayName("application.yml 提供 Swagger 和 auth 测试开关配置")
|
||||
void applicationYaml_containsSwaggerAndAuthToggle() throws Exception {
|
||||
PropertySource<?> source = new YamlPropertySourceLoader()
|
||||
.load("application", new ClassPathResource("application.yml"))
|
||||
.get(0);
|
||||
@@ -24,10 +24,10 @@ class ApplicationConfigTest {
|
||||
assertThat(source.getProperty("springdoc.api-docs.path")).isEqualTo("/v3/api-docs");
|
||||
assertThat(source.getProperty("springdoc.swagger-ui.enabled")).isEqualTo(true);
|
||||
assertThat(source.getProperty("springdoc.swagger-ui.path")).isEqualTo("/swagger-ui.html");
|
||||
assertThat(source.getProperty("shiro.auth.enabled")).isEqualTo(true);
|
||||
assertThat(source.getProperty("shiro.auth.mock-company-id")).isEqualTo(1);
|
||||
assertThat(source.getProperty("shiro.auth.mock-user-id")).isEqualTo(1);
|
||||
assertThat(source.getProperty("shiro.auth.mock-role")).isEqualTo("ADMIN");
|
||||
assertThat(source.getProperty("auth.enabled")).isEqualTo(true);
|
||||
assertThat(source.getProperty("auth.mock-company-id")).isEqualTo(1);
|
||||
assertThat(source.getProperty("auth.mock-user-id")).isEqualTo(1);
|
||||
assertThat(source.getProperty("auth.mock-role")).isEqualTo("ADMIN");
|
||||
assertThat(source.getProperty("logging.level.com.label")).isEqualTo("INFO");
|
||||
}
|
||||
|
||||
|
||||
151
src/test/java/com/label/unit/AuthInterceptorTest.java
Normal file
151
src/test/java/com/label/unit/AuthInterceptorTest.java
Normal file
@@ -0,0 +1,151 @@
|
||||
package com.label.unit;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.label.annotation.RequireAuth;
|
||||
import com.label.annotation.RequireRole;
|
||||
import com.label.common.auth.TokenPrincipal;
|
||||
import com.label.common.context.CompanyContext;
|
||||
import com.label.common.context.UserContext;
|
||||
import com.label.interceptor.AuthInterceptor;
|
||||
import com.label.service.RedisService;
|
||||
import com.label.util.RedisUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("自定义认证鉴权拦截器测试")
|
||||
class AuthInterceptorTest {
|
||||
|
||||
private final RedisService redisService = mock(RedisService.class);
|
||||
private final AuthInterceptor interceptor = new AuthInterceptor(redisService, new ObjectMapper());
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
CompanyContext.clear();
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有效 Token 会注入 Principal、租户上下文并刷新 TTL")
|
||||
void validTokenInjectsPrincipalAndRefreshesTtl() throws Exception {
|
||||
ReflectionTestUtils.setField(interceptor, "authEnabled", true);
|
||||
ReflectionTestUtils.setField(interceptor, "tokenTtlSeconds", 7200L);
|
||||
when(redisService.hGetAll(RedisUtil.tokenKey("valid-token"))).thenReturn(Map.of(
|
||||
"userId", "10",
|
||||
"role", "ADMIN",
|
||||
"companyId", "20",
|
||||
"username", "admin"
|
||||
));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/test/admin");
|
||||
request.addHeader("Authorization", "Bearer valid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean proceed = interceptor.preHandle(request, response, handler("adminOnly"));
|
||||
|
||||
assertThat(proceed).isTrue();
|
||||
TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||
assertThat(principal.getUserId()).isEqualTo(10L);
|
||||
assertThat(principal.getRole()).isEqualTo("ADMIN");
|
||||
assertThat(CompanyContext.get()).isEqualTo(20L);
|
||||
assertThat(UserContext.get()).isSameAs(principal);
|
||||
verify(redisService).expire(RedisUtil.tokenKey("valid-token"), 7200L);
|
||||
verify(redisService).expire(RedisUtil.userSessionsKey(10L), 7200L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("角色继承规则允许 ADMIN 访问 REVIEWER 接口")
|
||||
void adminRoleInheritsReviewerRole() throws Exception {
|
||||
ReflectionTestUtils.setField(interceptor, "authEnabled", true);
|
||||
when(redisService.hGetAll(RedisUtil.tokenKey("admin-token"))).thenReturn(Map.of(
|
||||
"userId", "1",
|
||||
"role", "ADMIN",
|
||||
"companyId", "1",
|
||||
"username", "admin"
|
||||
));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/test/reviewer");
|
||||
request.addHeader("Authorization", "Bearer admin-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertThat(interceptor.preHandle(request, response, handler("reviewerOnly"))).isTrue();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("角色不足时返回 403")
|
||||
void insufficientRoleReturnsForbidden() throws Exception {
|
||||
ReflectionTestUtils.setField(interceptor, "authEnabled", true);
|
||||
when(redisService.hGetAll(RedisUtil.tokenKey("annotator-token"))).thenReturn(Map.of(
|
||||
"userId", "2",
|
||||
"role", "ANNOTATOR",
|
||||
"companyId", "1",
|
||||
"username", "annotator"
|
||||
));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/test/reviewer");
|
||||
request.addHeader("Authorization", "Bearer annotator-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertThat(interceptor.preHandle(request, response, handler("reviewerOnly"))).isFalse();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缺少 Token 时返回 401")
|
||||
void missingTokenReturnsUnauthorized() throws Exception {
|
||||
ReflectionTestUtils.setField(interceptor, "authEnabled", true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/test/admin");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertThat(interceptor.preHandle(request, response, handler("adminOnly"))).isFalse();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(redisService, never()).hGetAll(org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("请求完成后清理用户和公司 ThreadLocal")
|
||||
void afterCompletionClearsContexts() throws Exception {
|
||||
CompanyContext.set(20L);
|
||||
UserContext.set(new TokenPrincipal(10L, "ADMIN", 20L, "admin", "token"));
|
||||
|
||||
interceptor.afterCompletion(new MockHttpServletRequest(), new MockHttpServletResponse(),
|
||||
handler("adminOnly"), null);
|
||||
|
||||
assertThat(CompanyContext.get()).isEqualTo(-1L);
|
||||
assertThat(UserContext.get()).isNull();
|
||||
}
|
||||
|
||||
private static HandlerMethod handler(String methodName) throws NoSuchMethodException {
|
||||
Method method = TestController.class.getDeclaredMethod(methodName);
|
||||
return new HandlerMethod(new TestController(), method);
|
||||
}
|
||||
|
||||
private static class TestController {
|
||||
@RequireRole("ADMIN")
|
||||
void adminOnly() {
|
||||
}
|
||||
|
||||
@RequireRole("REVIEWER")
|
||||
void reviewerOnly() {
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
void authenticatedOnly() {
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/test/java/com/label/unit/CompanyServiceTest.java
Normal file
73
src/test/java/com/label/unit/CompanyServiceTest.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package com.label.unit;
|
||||
|
||||
import com.label.common.exception.BusinessException;
|
||||
import com.label.entity.SysCompany;
|
||||
import com.label.mapper.SysCompanyMapper;
|
||||
import com.label.mapper.SysUserMapper;
|
||||
import com.label.service.CompanyService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("公司管理服务测试")
|
||||
class CompanyServiceTest {
|
||||
|
||||
private final SysCompanyMapper companyMapper = mock(SysCompanyMapper.class);
|
||||
private final SysUserMapper userMapper = mock(SysUserMapper.class);
|
||||
private final CompanyService companyService = new CompanyService(companyMapper, userMapper);
|
||||
|
||||
@Test
|
||||
@DisplayName("创建公司时写入 ACTIVE 状态并保存公司代码")
|
||||
void createCompanyInsertsActiveCompany() {
|
||||
SysCompany company = companyService.create("测试公司", "TEST");
|
||||
|
||||
assertThat(company.getCompanyName()).isEqualTo("测试公司");
|
||||
assertThat(company.getCompanyCode()).isEqualTo("TEST");
|
||||
assertThat(company.getStatus()).isEqualTo("ACTIVE");
|
||||
verify(companyMapper).insert(any(SysCompany.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建公司时拒绝重复公司代码")
|
||||
void createCompanyRejectsDuplicateCode() {
|
||||
SysCompany existing = new SysCompany();
|
||||
existing.setId(1L);
|
||||
when(companyMapper.selectByCompanyCode("DEMO")).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> companyService.create("演示公司", "DEMO"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("公司代码已存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("禁用公司时只允许 ACTIVE 或 DISABLED")
|
||||
void updateStatusRejectsInvalidStatus() {
|
||||
SysCompany existing = new SysCompany();
|
||||
existing.setId(1L);
|
||||
existing.setStatus("ACTIVE");
|
||||
when(companyMapper.selectById(1L)).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> companyService.updateStatus(1L, "DELETED"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("公司状态不合法");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除公司时若仍有关联用户则拒绝删除")
|
||||
void deleteRejectsCompanyWithUsers() {
|
||||
SysCompany existing = new SysCompany();
|
||||
existing.setId(1L);
|
||||
when(companyMapper.selectById(1L)).thenReturn(existing);
|
||||
when(userMapper.countByCompanyId(1L)).thenReturn(2L);
|
||||
|
||||
assertThatThrownBy(() -> companyService.delete(1L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("公司下仍存在用户");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.label.unit;
|
||||
|
||||
import com.label.controller.AuthController;
|
||||
import com.label.controller.CompanyController;
|
||||
import com.label.controller.ExportController;
|
||||
import com.label.controller.ExtractionController;
|
||||
import com.label.controller.QaController;
|
||||
@@ -37,6 +38,7 @@ class OpenApiAnnotationTest {
|
||||
|
||||
private static final List<Class<?>> CONTROLLERS = List.of(
|
||||
AuthController.class,
|
||||
CompanyController.class,
|
||||
UserController.class,
|
||||
SourceController.class,
|
||||
TaskController.class,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.label.unit;
|
||||
|
||||
import com.label.common.shiro.UserRealm;
|
||||
import com.label.config.ShiroConfig;
|
||||
import com.label.service.RedisService;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@DisplayName("ShiroConfig 单元测试")
|
||||
class ShiroConfigTest {
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
org.apache.shiro.util.ThreadContext.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("securityManager 不应依赖 DefaultWebSecurityManager,以避免 javax.servlet 兼容性问题")
|
||||
void securityManager_shouldNotDependOnDefaultWebSecurityManager() {
|
||||
ShiroConfig config = new ShiroConfig();
|
||||
RedisService redisService = mock(RedisService.class);
|
||||
UserRealm realm = config.userRealm(redisService);
|
||||
|
||||
SecurityManager securityManager = config.securityManager(realm);
|
||||
|
||||
assertThat(securityManager).isNotInstanceOf(DefaultWebSecurityManager.class);
|
||||
ThreadContext.bind(securityManager);
|
||||
assertThatCode(SecurityUtils::getSubject).doesNotThrowAnyException();
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.label.unit;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.label.common.context.CompanyContext;
|
||||
import com.label.common.shiro.TokenFilter;
|
||||
import com.label.common.shiro.TokenPrincipal;
|
||||
import com.label.common.shiro.UserRealm;
|
||||
import com.label.config.ShiroConfig;
|
||||
import com.label.service.RedisService;
|
||||
import com.label.util.RedisUtil;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@DisplayName("TokenFilter 单元测试")
|
||||
class TokenFilterTest {
|
||||
|
||||
private RedisService redisService;
|
||||
private TestableTokenFilter filter;
|
||||
private DefaultSecurityManager securityManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
redisService = mock(RedisService.class);
|
||||
UserRealm userRealm = new UserRealm(redisService);
|
||||
securityManager = (DefaultSecurityManager) new ShiroConfig().securityManager(userRealm);
|
||||
filter = new TestableTokenFilter(redisService, new ObjectMapper(), securityManager);
|
||||
SecurityUtils.setSecurityManager(securityManager);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
CompanyContext.clear();
|
||||
ThreadContext.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有效 Token 请求会刷新 token TTL,实现滑动过期")
|
||||
void validToken_refreshesTokenTtl() throws Exception {
|
||||
ReflectionTestUtils.setField(filter, "authEnabled", true);
|
||||
ReflectionTestUtils.setField(filter, "tokenTtlSeconds", 7200L);
|
||||
String token = "valid-token";
|
||||
when(redisService.hGetAll(RedisUtil.tokenKey(token))).thenReturn(Map.of(
|
||||
"userId", "10",
|
||||
"role", "ADMIN",
|
||||
"companyId", "20",
|
||||
"username", "admin"
|
||||
));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/tasks");
|
||||
request.addHeader("Authorization", "Bearer " + token);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RecordingChain chain = new RecordingChain();
|
||||
|
||||
filter.invoke(request, response, chain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(chain.principal).isInstanceOf(TokenPrincipal.class);
|
||||
assertThat(chain.roleChecked).isTrue();
|
||||
verify(redisService).expire(RedisUtil.tokenKey(token), 7200L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("shiro.auth.enabled=false 时注入 mock Principal 并跳过 Redis 校验")
|
||||
void authDisabled_injectsMockPrincipalWithoutRedisLookup() throws Exception {
|
||||
ReflectionTestUtils.setField(filter, "authEnabled", false);
|
||||
ReflectionTestUtils.setField(filter, "mockCompanyId", 3L);
|
||||
ReflectionTestUtils.setField(filter, "mockUserId", 4L);
|
||||
ReflectionTestUtils.setField(filter, "mockRole", "ADMIN");
|
||||
ReflectionTestUtils.setField(filter, "mockUsername", "mock-admin");
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/tasks");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RecordingChain chain = new RecordingChain();
|
||||
|
||||
filter.invoke(request, response, chain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
TokenPrincipal principal = chain.principal;
|
||||
assertThat(principal.getCompanyId()).isEqualTo(3L);
|
||||
assertThat(principal.getUserId()).isEqualTo(4L);
|
||||
assertThat(principal.getRole()).isEqualTo("ADMIN");
|
||||
assertThat(principal.getUsername()).isEqualTo("mock-admin");
|
||||
assertThat(chain.roleChecked).isTrue();
|
||||
verify(redisService, never()).hGetAll(anyString());
|
||||
}
|
||||
|
||||
private static final class RecordingChain implements FilterChain {
|
||||
private TokenPrincipal principal;
|
||||
private boolean roleChecked;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response) {
|
||||
principal = (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||
SecurityUtils.getSubject().checkRole(principal.getRole());
|
||||
roleChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestableTokenFilter extends TokenFilter {
|
||||
private TestableTokenFilter(RedisService redisService, ObjectMapper objectMapper,
|
||||
DefaultSecurityManager securityManager) {
|
||||
super(redisService, objectMapper);
|
||||
}
|
||||
|
||||
private void invoke(MockHttpServletRequest request, MockHttpServletResponse response, FilterChain chain)
|
||||
throws Exception {
|
||||
super.doFilterInternal(request, response, chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user