Browse Source

api接口验签

master
qimaohong 1 month ago
parent
commit
9e5fe777c0
  1. 1
      .gitignore
  2. 2406
      logs/application-test.log
  3. 6
      pom.xml
  4. 110
      src/test/java/com/deepchart/common/sign/ApiSignPropertiesTest.java
  5. 266
      src/test/java/com/deepchart/common/sign/SignInterceptorTest.java
  6. 260
      src/test/java/com/deepchart/common/sign/SignUtilTest.java
  7. 52
      src/test/java/com/deepchart/controller/PublicControllerTest.java
  8. 203
      src/test/java/com/deepchart/controller/UserControllerTest.java
  9. 222
      src/test/java/com/deepchart/integration/ApiSignIntegrationTest.java

1
.gitignore

@ -31,3 +31,4 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
test/

2406
logs/application-test.log
File diff suppressed because it is too large
View File

6
pom.xml

@ -71,6 +71,12 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

110
src/test/java/com/deepchart/common/sign/ApiSignPropertiesTest.java

@ -0,0 +1,110 @@
package com.deepchart.common.sign;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@DisplayName("API签名配置测试")
class ApiSignPropertiesTest {
private ApiSignProperties apiSignProperties;
@BeforeEach
void setUp() {
apiSignProperties = new ApiSignProperties();
Map<String, String> appSecrets = new HashMap<>();
appSecrets.put("web-app", "web_secret_123");
appSecrets.put("mobile-app", "mobile_secret_456");
appSecrets.put("third-party", "third_party_secret_789");
apiSignProperties.setAppSecrets(appSecrets);
apiSignProperties.setEnabled(true);
apiSignProperties.setExpireTime(300000L);
apiSignProperties.setIncludePatterns(new String[]{"/api/**"});
apiSignProperties.setExcludePatterns(new String[]{"/api/public/**"});
}
@Test
@DisplayName("获取应用密钥 - 存在")
void testGetAppSecret_Exists() {
// 执行
String secret = apiSignProperties.getAppSecret("web-app");
// 验证
assertEquals("web_secret_123", secret);
}
@Test
@DisplayName("获取应用密钥 - 不存在")
void testGetAppSecret_NotExists() {
// 执行
String secret = apiSignProperties.getAppSecret("non-existent-app");
// 验证
assertNull(secret);
}
@Test
@DisplayName("检查应用ID - 存在")
void testContainsAppId_Exists() {
// 执行 & 验证
assertTrue(apiSignProperties.containsAppId("web-app"));
assertTrue(apiSignProperties.containsAppId("mobile-app"));
assertTrue(apiSignProperties.containsAppId("third-party"));
}
@Test
@DisplayName("检查应用ID - 不存在")
void testContainsAppId_NotExists() {
// 执行 & 验证
assertFalse(apiSignProperties.containsAppId("non-existent-app"));
assertFalse(apiSignProperties.containsAppId(""));
assertFalse(apiSignProperties.containsAppId(null));
}
@Test
@DisplayName("默认值测试")
void testDefaultValues() {
// 准备
ApiSignProperties defaultProperties = new ApiSignProperties();
// 验证
assertTrue(defaultProperties.isEnabled());
assertEquals(300000L, defaultProperties.getExpireTime());
assertNotNull(defaultProperties.getAppSecrets());
assertNotNull(defaultProperties.getIncludePatterns());
assertNotNull(defaultProperties.getExcludePatterns());
}
@Test
@DisplayName("Setter/Getter测试")
void testSettersAndGetters() {
// 准备
ApiSignProperties properties = new ApiSignProperties();
// 执行
properties.setEnabled(false);
properties.setExpireTime(600000L);
properties.setIncludePatterns(new String[]{"/secure/**"});
properties.setExcludePatterns(new String[]{"/secure/public/**"});
Map<String, String> secrets = new HashMap<>();
secrets.put("test-app", "test_secret");
properties.setAppSecrets(secrets);
// 验证
assertFalse(properties.isEnabled());
assertEquals(600000L, properties.getExpireTime());
assertArrayEquals(new String[]{"/secure/**"}, properties.getIncludePatterns());
assertArrayEquals(new String[]{"/secure/public/**"}, properties.getExcludePatterns());
assertEquals("test_secret", properties.getAppSecret("test-app"));
}
}

266
src/test/java/com/deepchart/common/sign/SignInterceptorTest.java

@ -0,0 +1,266 @@
package com.deepchart.common.sign;
import com.deepchart.common.exception.SignatureException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.util.AntPathMatcher;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("签名拦截器测试")
class SignInterceptorTest {
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Mock
private Object handler;
private ApiSignProperties apiSignProperties;
private SignInterceptor signInterceptor;
@BeforeEach
void setUp() {
apiSignProperties = new ApiSignProperties();
apiSignProperties.setEnabled(true);
Map<String, String> appSecrets = new HashMap<>();
appSecrets.put("web-app", "web_app_secret_2024");
appSecrets.put("mobile-app", "mobile_app_secret_2024");
apiSignProperties.setAppSecrets(appSecrets);
apiSignProperties.setIncludePatterns(new String[]{"/api/**"});
apiSignProperties.setExcludePatterns(new String[]{"/api/actuator/**", "/api/public/**"});
signInterceptor = new SignInterceptor(apiSignProperties);
}
@Test
@DisplayName("拦截器 - 签名验证通过")
void testPreHandle_Success() throws Exception {
// 准备
when(request.getRequestURI()).thenReturn("/api/users");
setupValidSignatureHeaders();
// 执行
boolean result = signInterceptor.preHandle(request, response, handler);
// 验证
assertTrue(result, "签名验证通过应返回true");
}
@Test
@DisplayName("拦截器 - 签名验证失败")
void testPreHandle_SignatureFailed() {
// 准备
when(request.getRequestURI()).thenReturn("/api/users");
setupInvalidSignatureHeaders();
// 执行 & 验证
SignatureException exception = assertThrows(SignatureException.class,
() -> signInterceptor.preHandle(request, response, handler));
assertEquals("签名验证失败", exception.getMessage());
}
@Test
@DisplayName("拦截器 - 无效appId")
void testPreHandle_InvalidAppId() {
// 准备
when(request.getRequestURI()).thenReturn("/api/users");
setupHeadersWithInvalidAppId();
// 执行 & 验证
SignatureException exception = assertThrows(SignatureException.class,
() -> signInterceptor.preHandle(request, response, handler));
assertEquals("应用ID无效", exception.getMessage());
}
@Test
@DisplayName("拦截器 - 缺少必要参数")
void testPreHandle_MissingRequiredParams() {
// 准备
when(request.getRequestURI()).thenReturn("/api/users");
setupHeadersWithMissingParams();
// 执行 & 验证
SignatureException exception = assertThrows(SignatureException.class,
() -> signInterceptor.preHandle(request, response, handler));
assertEquals("签名参数缺失", exception.getMessage());
}
@Test
@DisplayName("拦截器 - 排除路径不验证")
void testPreHandle_ExcludedPath() throws Exception {
// 准备
when(request.getRequestURI()).thenReturn("/api/actuator/health");
// 执行
boolean result = signInterceptor.preHandle(request, response, handler);
// 验证
assertTrue(result, "排除路径应直接通过");
verify(request, never()).getHeader(anyString());
}
@Test
@DisplayName("拦截器 - 非包含路径不验证")
void testPreHandle_NotIncludedPath() throws Exception {
// 准备
when(request.getRequestURI()).thenReturn("/other/path");
// 执行
boolean result = signInterceptor.preHandle(request, response, handler);
// 验证
assertTrue(result, "非包含路径应直接通过");
verify(request, never()).getHeader(anyString());
}
@Test
@DisplayName("拦截器 - 签名验证禁用")
void testPreHandle_SignDisabled() throws Exception {
// 准备
apiSignProperties.setEnabled(false);
when(request.getRequestURI()).thenReturn("/api/users");
// 执行
boolean result = signInterceptor.preHandle(request, response, handler);
// 验证
assertTrue(result, "签名验证禁用时应直接通过");
verify(request, never()).getHeader(anyString());
}
@Test
@DisplayName("提取签名参数 - 从Header")
void testExtractSignParams_FromHeaders() {
// 准备
setupValidSignatureHeaders();
// 执行 - 通过反射调用私有方法
Map<String, String> params = extractSignParams();
// 验证
assertNotNull(params);
assertEquals("web-app", params.get("appid"));
assertEquals("1640995200000", params.get("timestamp"));
assertEquals("abc123def456", params.get("nonce"));
assertEquals("valid_signature", params.get("sign"));
}
@Test
@DisplayName("提取签名参数 - 从URL参数")
void testExtractSignParams_FromParameters() {
// 准备
when(request.getRequestURI()).thenReturn("/api/users");
// 设置URL参数
when(request.getParameter("appId")).thenReturn("web-app");
when(request.getParameter("timestamp")).thenReturn("1640995200000");
when(request.getParameter("nonce")).thenReturn("abc123def456");
when(request.getParameter("sign")).thenReturn("valid_signature");
// 执行 - 通过反射调用私有方法
Map<String, String> params = extractSignParams();
// 验证
assertNotNull(params);
assertEquals("web-app", params.get("appid"));
assertEquals("1640995200000", params.get("timestamp"));
assertEquals("abc123def456", params.get("nonce"));
assertEquals("valid_signature", params.get("sign"));
}
// 辅助方法设置有效的签名Header
private void setupValidSignatureHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("X-API-APPID", "web-app");
headers.put("X-API-TIMESTAMP", "1640995200000");
headers.put("X-API-NONCE", "abc123def456");
headers.put("X-API-SIGN", "valid_signature");
setupHeaders(headers);
// 设置签名验证通过
Map<String, String> signParams = new HashMap<>();
signParams.put("appid", "web-app");
signParams.put("timestamp", "1640995200000");
signParams.put("nonce", "abc123def456");
signParams.put("sign", "valid_signature");
// 这里需要模拟SignUtil.verifySign返回true
// 实际项目中可以使用PowerMock或重构代码使其可测试
}
// 辅助方法设置无效的签名Header
private void setupInvalidSignatureHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("X-API-APPID", "web-app");
headers.put("X-API-TIMESTAMP", "1640995200000");
headers.put("X-API-NONCE", "abc123def456");
headers.put("X-API-SIGN", "invalid_signature");
setupHeaders(headers);
}
// 辅助方法设置无效的appId
private void setupHeadersWithInvalidAppId() {
Map<String, String> headers = new HashMap<>();
headers.put("X-API-APPID", "invalid-app");
headers.put("X-API-TIMESTAMP", "1640995200000");
headers.put("X-API-NONCE", "abc123def456");
headers.put("X-API-SIGN", "some_signature");
setupHeaders(headers);
}
// 辅助方法设置缺少参数的Header
private void setupHeadersWithMissingParams() {
Map<String, String> headers = new HashMap<>();
headers.put("X-API-APPID", "web-app");
// 缺少timestampnoncesign
setupHeaders(headers);
}
// 辅助方法设置Header
private void setupHeaders(Map<String, String> headers) {
Vector<String> headerNames = new Vector<>(headers.keySet());
when(request.getHeaderNames()).thenReturn(headerNames.elements());
for (Map.Entry<String, String> entry : headers.entrySet()) {
when(request.getHeader(entry.getKey())).thenReturn(entry.getValue());
}
}
// 辅助方法通过反射调用私有方法extractSignParams
private Map<String, String> extractSignParams() {
try {
var method = SignInterceptor.class.getDeclaredMethod("extractSignParams", HttpServletRequest.class);
method.setAccessible(true);
return (Map<String, String>) method.invoke(signInterceptor, request);
} catch (Exception e) {
throw new RuntimeException("反射调用失败", e);
}
}
}

260
src/test/java/com/deepchart/common/sign/SignUtilTest.java

@ -0,0 +1,260 @@
package com.deepchart.common.sign;
import com.deepchart.common.util.SignUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("签名工具测试")
class SignUtilTest {
private static final String APP_SECRET = "test_secret_2024";
private Map<String, String> testParams;
@BeforeEach
void setUp() {
testParams = new HashMap<>();
testParams.put("appId", "test-app");
testParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
testParams.put("nonce", "abc123def456");
testParams.put("username", "testuser");
testParams.put("email", "test@example.com");
}
@Test
@DisplayName("生成签名 - 正常情况")
void testGenerateSign_NormalCase() {
// 执行
String sign = SignUtil.generateSign(testParams, APP_SECRET);
// 验证
assertNotNull(sign, "签名不应为null");
assertEquals(32, sign.length(), "MD5签名长度应为32");
assertTrue(sign.matches("^[a-f0-9]{32}$"), "签名应为32位十六进制字符串");
}
@Test
@DisplayName("生成签名 - 参数排序")
void testGenerateSign_ParameterOrder() {
// 准备不同顺序的参数
Map<String, String> params1 = new HashMap<>();
params1.put("z", "value1");
params1.put("a", "value2");
params1.put("m", "value3");
Map<String, String> params2 = new HashMap<>();
params2.put("a", "value2");
params2.put("m", "value3");
params2.put("z", "value1");
// 执行
String sign1 = SignUtil.generateSign(params1, APP_SECRET);
String sign2 = SignUtil.generateSign(params2, APP_SECRET);
// 验证
assertEquals(sign1, sign2, "不同参数顺序应生成相同签名");
System.out.println("sign1: " + sign1);
System.out.println("sign2: " + sign2);
}
@Test
@DisplayName("验证签名 - 正确签名")
void testVerifySign_CorrectSign() {
// 准备
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertTrue(isValid, "正确签名应验证通过");
}
@Test
@DisplayName("验证签名 - 错误签名")
void testVerifySign_WrongSign() {
// 准备
String correctSign = SignUtil.generateSign(testParams, APP_SECRET);
String wrongSign = "wrong_signature_1234567890";
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, wrongSign);
// 验证
assertFalse(isValid, "错误签名应验证失败");
}
@Test
@DisplayName("验证签名 - 过期时间戳")
void testVerifySign_ExpiredTimestamp() {
// 准备过期时间戳超过5分钟
long expiredTimestamp = System.currentTimeMillis() - 6 * 60 * 1000;
testParams.put("timestamp", String.valueOf(expiredTimestamp));
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "过期时间戳应验证失败");
}
@Test
@DisplayName("验证签名 - 未来时间戳")
void testVerifySign_FutureTimestamp() {
// 准备未来时间戳超过5分钟
long futureTimestamp = System.currentTimeMillis() + 6 * 60 * 1000;
testParams.put("timestamp", String.valueOf(futureTimestamp));
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "未来时间戳应验证失败");
}
@Test
@DisplayName("验证签名 - 无效时间戳格式")
void testVerifySign_InvalidTimestampFormat() {
// 准备无效时间戳
testParams.put("timestamp", "invalid_timestamp");
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "无效时间戳格式应验证失败");
}
@Test
@DisplayName("验证签名 - 缺少时间戳")
void testVerifySign_MissingTimestamp() {
// 准备缺少时间戳的参数
testParams.remove("timestamp");
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "缺少时间戳应验证失败");
}
@Test
@DisplayName("验证签名 - 无效nonce")
void testVerifySign_InvalidNonce() {
// 准备无效nonce
testParams.put("nonce", "short"); // 太短的nonce
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "无效nonce应验证失败");
}
@ParameterizedTest
@NullAndEmptySource
@DisplayName("验证签名 - 空或null参数值")
void testVerifySign_NullOrEmptyParams(String value) {
// 准备空或null参数
testParams.put("nonce", value);
String sign = SignUtil.generateSign(testParams, APP_SECRET);
testParams.put("sign", sign);
// 执行
boolean isValid = SignUtil.verifySign(testParams, APP_SECRET, sign);
// 验证
assertFalse(isValid, "空或null参数应验证失败");
}
@Test
@DisplayName("生成随机字符串")
void testGenerateNonce() {
// 执行
String nonce1 = SignUtil.generateNonce();
String nonce2 = SignUtil.generateNonce();
// 验证
assertNotNull(nonce1, "nonce不应为null");
assertNotNull(nonce2, "nonce不应为null");
assertEquals(16, nonce1.length(), "nonce长度应为16");
assertEquals(16, nonce2.length(), "nonce长度应为16");
assertNotEquals(nonce1, nonce2, "两次生成的nonce应不同");
assertTrue(nonce1.matches("^[a-f0-9]{16}$"), "nonce应为16位十六进制字符串");
System.out.println(nonce1);
System.out.println(nonce2);
}
@Test
@DisplayName("获取当前时间戳")
void testGetCurrentTimestamp() {
// 执行
String timestamp = SignUtil.getCurrentTimestamp();
long currentTime = System.currentTimeMillis();
// 验证
assertNotNull(timestamp, "时间戳不应为null");
long parsedTimestamp = Long.parseLong(timestamp);
assertTrue(Math.abs(currentTime - parsedTimestamp) < 1000,
"生成的时间戳应与当前时间相近");
System.out.println(timestamp);
}
@Test
@DisplayName("生成签名 - 空参数")
void testGenerateSign_EmptyParams() {
// 准备空参数
Map<String, String> emptyParams = new HashMap<>();
emptyParams.put("appId", "test-app");
emptyParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
emptyParams.put("nonce", "abc123def456");
// 执行
String sign = SignUtil.generateSign(emptyParams, APP_SECRET);
// 验证
assertNotNull(sign, "空参数也应生成签名");
assertEquals(32, sign.length(), "签名长度应为32");
System.out.println("签名: "+ sign);
}
@Test
@DisplayName("生成签名 - 特殊字符参数")
void testGenerateSign_SpecialCharacters() {
// 准备包含特殊字符的参数
testParams.put("special", "!@#$%^&*()_+-=[]{}|;:,.<>?");
testParams.put("unicode", "中文测试");
testParams.put("space", "value with spaces");
// 执行
String sign = SignUtil.generateSign(testParams, APP_SECRET);
// 验证
assertNotNull(sign, "特殊字符参数应生成签名");
assertEquals(32, sign.length(), "签名长度应为32");
}
}

52
src/test/java/com/deepchart/controller/PublicControllerTest.java

@ -0,0 +1,52 @@
package com.deepchart.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(PublicController.class)
@DisplayName("公开接口控制器测试")
class PublicControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("获取服务器时间 - 无需签名")
void testGetServerTimestamp_NoSignatureRequired() throws Exception {
// 执行 & 验证 - 不需要签名Header
mockMvc.perform(get("/api/public/timestamp"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.timestamp").exists())
.andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"));
}
@Test
@DisplayName("健康检查 - 无需签名")
void testHealth_NoSignatureRequired() throws Exception {
// 执行 & 验证 - 不需要签名Header
mockMvc.perform(get("/api/public/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("Service is healthy"));
}
@Test
@DisplayName("公开接口 - 即使提供签名也不验证")
void testPublicEndpoint_WithSignature() throws Exception {
// 执行 & 验证 - 即使提供签名Header公开接口也不验证
mockMvc.perform(get("/api/public/timestamp")
.header("X-API-APPID", "web-app")
.header("X-API-TIMESTAMP", "1640995200000")
.header("X-API-NONCE", "abc123def456")
.header("X-API-SIGN", "any_signature"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
}

203
src/test/java/com/deepchart/controller/UserControllerTest.java

@ -0,0 +1,203 @@
package com.deepchart.controller;
import com.deepchart.common.util.SignUtil;
import com.deepchart.entity.User;
import com.deepchart.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
@DisplayName("用户控制器测试")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
private static final String APP_ID = "web-app";
private static final String APP_SECRET = "web_app_secret_2024";
private Map<String, String> signParams;
@BeforeEach
void setUp() {
signParams = new HashMap<>();
signParams.put("appId", APP_ID);
signParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
signParams.put("nonce", SignUtil.generateNonce());
}
@Test
@DisplayName("创建用户 - 成功")
void testCreateUser_Success() throws Exception {
// 准备
User user = createTestUser();
User savedUser = createTestUser();
savedUser.setId(1L);
when(userService.createUser(any(User.class))).thenReturn(savedUser);
// 生成签名
String sign = generateSign();
// 执行 & 验证
mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", sign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("用户创建成功"))
.andExpect(jsonPath("$.data.id").value(1));
}
@Test
@DisplayName("创建用户 - 签名验证失败")
void testCreateUser_SignatureFailed() throws Exception {
// 准备
User user = createTestUser();
// 执行 & 验证 - 使用错误的签名
mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", "wrong_signature")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk()) // 注意我们的异常处理返回200状态但code是401
.andExpect(jsonPath("$.code").value(1003)) // SIGNATURE_ERROR
.andExpect(jsonPath("$.message").value("签名验证失败"));
}
@Test
@DisplayName("创建用户 - 缺少签名参数")
void testCreateUser_MissingSignatureParams() throws Exception {
// 准备
User user = createTestUser();
// 执行 & 验证 - 不提供签名Header
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(1003)) // SIGNATURE_ERROR
.andExpect(jsonPath("$.message").value("签名参数缺失"));
}
@Test
@DisplayName("获取用户列表 - 成功")
void testListUsers_Success() throws Exception {
// 准备
Page<User> userPage = new PageImpl<>(Collections.singletonList(createTestUser()));
when(userService.listUsers(any(PageRequest.class))).thenReturn(userPage);
// 生成签名
String sign = generateSign();
// 执行 & 验证
mockMvc.perform(get("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", sign)
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.content").isArray());
}
@Test
@DisplayName("获取用户详情 - 成功")
void testGetUserById_Success() throws Exception {
// 准备
User user = createTestUser();
user.setId(1L);
when(userService.getUserById(1L)).thenReturn(user);
// 生成签名
String sign = generateSign();
// 执行 & 验证
mockMvc.perform(get("/api/users/1")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", sign))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.id").value(1));
}
@Test
@DisplayName("用户登录 - 成功")
void testLogin_Success() throws Exception {
// 准备
User user = createTestUser();
user.setId(1L);
when(userService.login("testuser", "password123")).thenReturn(user);
// 生成签名 - 包含登录参数
Map<String, String> loginParams = new HashMap<>(signParams);
loginParams.put("username", "testuser");
loginParams.put("password", "password123");
String sign = SignUtil.generateSign(loginParams, APP_SECRET);
// 执行 & 验证
mockMvc.perform(post("/api/users/login")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", sign)
.param("username", "testuser")
.param("password", "password123"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("登录成功"))
.andExpect(jsonPath("$.data.id").value(1));
}
// 辅助方法创建测试用户
private User createTestUser() {
User user = new User();
user.setUsername("testuser");
user.setPassword("password123");
user.setEmail("test@example.com");
user.setNickname("Test User");
user.setPhone("13800138000");
return user;
}
// 辅助方法生成签名
private String generateSign() {
return SignUtil.generateSign(signParams, APP_SECRET);
}
}

222
src/test/java/com/deepchart/integration/ApiSignIntegrationTest.java

@ -0,0 +1,222 @@
package com.deepchart.integration;
import com.deepchart.common.util.SignUtil;
import com.deepchart.entity.User;
import com.deepchart.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
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.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
@DisplayName("API签名集成测试")
class ApiSignIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
private static final String APP_ID = "web-app";
private static final String APP_SECRET = "web_app_secret_2024";
private Map<String, String> signParams;
@BeforeEach
void setUp() {
signParams = new HashMap<>();
signParams.put("appId", APP_ID);
signParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
signParams.put("nonce", SignUtil.generateNonce());
}
@Test
@DisplayName("完整流程 - 创建用户并查询")
void testCompleteFlow_CreateAndGetUser() throws Exception {
// 准备用户数据
User user = new User();
user.setUsername("integration_test_user");
user.setPassword("password123");
user.setEmail("integration@test.com");
user.setNickname("Integration Test User");
// 生成创建用户的签名
String createSign = generateSign();
// 1. 创建用户
String responseJson = mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", createSign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.id").exists())
.andReturn().getResponse().getContentAsString();
// 提取用户ID
var response = objectMapper.readTree(responseJson);
Long userId = response.path("data").path("id").asLong();
// 更新签名参数用于查询
signParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
signParams.put("nonce", SignUtil.generateNonce());
String getSign = generateSign();
// 2. 查询用户
mockMvc.perform(get("/api/users/{id}", userId)
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", getSign))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.id").value(userId))
.andExpect(jsonPath("$.data.username").value("integration_test_user"));
}
@Test
@DisplayName("重放攻击防护 - 相同的nonce不能重复使用")
void testReplayAttackProtection() throws Exception {
// 准备
User user = new User();
user.setUsername("replay_test_user");
user.setPassword("password123");
user.setEmail("replay@test.com");
// 第一次请求 - 应该成功
String firstSign = generateSign();
mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", firstSign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
// 第二次使用相同的nonce - 应该失败时间戳可能过期或nonce重复
// 注意实际的重放攻击防护需要在服务端缓存已使用的nonce
// 这里主要测试时间戳过期的情况
// 使用过期的时间戳
long expiredTimestamp = System.currentTimeMillis() - 6 * 60 * 1000;
signParams.put("timestamp", String.valueOf(expiredTimestamp));
String expiredSign = generateSign();
mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", expiredSign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(1003)); // 签名验证失败
}
@Test
@DisplayName("时间戳验证 - 过期时间戳被拒绝")
void testTimestampValidation_ExpiredTimestamp() throws Exception {
// 准备过期时间戳
long expiredTimestamp = System.currentTimeMillis() - 6 * 60 * 1000;
signParams.put("timestamp", String.valueOf(expiredTimestamp));
String expiredSign = generateSign();
User user = new User();
user.setUsername("expired_timestamp_user");
user.setPassword("password123");
// 执行 & 验证
mockMvc.perform(post("/api/users")
.header("X-API-APPID", APP_ID)
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", expiredSign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(1003)) // SIGNATURE_ERROR
.andExpect(jsonPath("$.message").value("签名验证失败"));
}
@Test
@DisplayName("不同应用ID - 使用不同的密钥")
void testDifferentAppIds_DifferentSecrets() throws Exception {
// 测试使用不同的appId和对应的密钥
// 使用mobile-app
Map<String, String> mobileParams = new HashMap<>();
mobileParams.put("appId", "mobile-app");
mobileParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
mobileParams.put("nonce", SignUtil.generateNonce());
String mobileSign = SignUtil.generateSign(mobileParams, "mobile_app_secret_2024");
User user = new User();
user.setUsername("mobile_test_user");
user.setPassword("password123");
mockMvc.perform(post("/api/users")
.header("X-API-APPID", "mobile-app")
.header("X-API-TIMESTAMP", mobileParams.get("timestamp"))
.header("X-API-NONCE", mobileParams.get("nonce"))
.header("X-API-SIGN", mobileSign)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
@DisplayName("参数篡改检测 - 修改参数导致签名不匹配")
void testParameterTamperingDetection() throws Exception {
// 准备正确的签名
String correctSign = generateSign();
User user = new User();
user.setUsername("tamper_test_user");
user.setPassword("password123");
// 尝试篡改参数 - 使用错误的appId但正确的签名
mockMvc.perform(post("/api/users")
.header("X-API-APPID", "wrong-app") // 篡改的appId
.header("X-API-TIMESTAMP", signParams.get("timestamp"))
.header("X-API-NONCE", signParams.get("nonce"))
.header("X-API-SIGN", correctSign) // 使用原始签名
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(1003)) // 签名验证失败
.andExpect(jsonPath("$.message").value("签名验证失败"));
}
// 辅助方法生成签名
private String generateSign() {
return SignUtil.generateSign(signParams, APP_SECRET);
}
}
Loading…
Cancel
Save