9 changed files with 3526 additions and 0 deletions
-
1.gitignore
-
2406logs/application-test.log
-
6pom.xml
-
110src/test/java/com/deepchart/common/sign/ApiSignPropertiesTest.java
-
266src/test/java/com/deepchart/common/sign/SignInterceptorTest.java
-
260src/test/java/com/deepchart/common/sign/SignUtilTest.java
-
52src/test/java/com/deepchart/controller/PublicControllerTest.java
-
203src/test/java/com/deepchart/controller/UserControllerTest.java
-
222src/test/java/com/deepchart/integration/ApiSignIntegrationTest.java
2406
logs/application-test.log
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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")); |
||||
|
} |
||||
|
} |
||||
@ -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"); |
||||
|
// 缺少timestamp、nonce、sign |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue