去掉shiro框架

This commit is contained in:
wh
2026-04-14 16:33:34 +08:00
parent 158873d5ae
commit a30b648d30
44 changed files with 868 additions and 859 deletions

View File

@@ -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 TokencompanyId=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 TokencompanyId=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);
}
}

View File

@@ -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");
}

View 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() {
}
}
}

View 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("公司下仍存在用户");
}
}

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}