feat(sae): add Java MVP backend and Taobao connectivity page
Spring Boot 3.5.6 / Java 17 JAR with cloud gateway envelope and ping/whoami/echo; Taobao gateway signature verification matching the Node implementation; /pages/mvp page, splash dev entry and local transport contract test; SAE + 聚石塔 runbook and portable JDK/Maven setup; Windows 11 wmic shim for Taobao DevTools.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# taobao-mvp-backend
|
||||
|
||||
淘宝小程序 + 淘宝云网关 + SAE 的最小 Java 后端验证工程。
|
||||
|
||||
## 快速启动
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process Bypass
|
||||
. D:\juzi\tools\env-java.ps1
|
||||
cd D:\juzi\backend-java
|
||||
mvn -DskipTests package
|
||||
java -jar target\taobao-mvp-backend-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
启动后:
|
||||
|
||||
- 健康检查:<http://127.0.0.1:8080/actuator/health>
|
||||
- 云网关测试接口:`POST http://127.0.0.1:8080/api/taobao/ping`
|
||||
|
||||
## 接口
|
||||
|
||||
| 方法 | 路径 | 作用 |
|
||||
|---|---|---|
|
||||
| GET | `/actuator/health` | SAE 健康检查 |
|
||||
| GET/POST | `/api/taobao/ping` | 返回统一 envelope,验证连通性 |
|
||||
| POST | `/api/taobao/whoami` | 返回网关注入的 query 参数(open_id/app_key/env/sign 等) |
|
||||
| POST | `/api/taobao/echo` | 原样回显 JSON body,验证 POST/Content-Type |
|
||||
|
||||
统一响应:
|
||||
|
||||
```json
|
||||
{"success":true,"data":{"ok":true}}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```json
|
||||
{"success":false,"errorCode":"SIGN_INVALID","errorMessage":"..."}
|
||||
```
|
||||
|
||||
## 环境变量(SAE)
|
||||
|
||||
| 变量 | 本地 | 云端 |
|
||||
|---|---|---|
|
||||
| `TAOBAO_APP_KEY` | 可空 | 淘宝开放平台 AppKey |
|
||||
| `TAOBAO_APP_SECRET` | 可空 | AppSecret,只放服务端 |
|
||||
| `TAOBAO_VERIFY_SIGN` | `false` | `true` |
|
||||
|
||||
云端必须用 `TAOBAO_VERIFY_SIGN=true`,否则任何知道地址的人都能直接调用接口。
|
||||
|
||||
## 说明
|
||||
|
||||
- 本工程只实现连通性 MVP:`ping/whoami/echo`。
|
||||
- 真实的 `challenge/session/photo/skin-analyze/analysis/reports` 尚未迁移;现有 Node 实现见 `../h5/lib/taobao-routes.js`,可作为 Java 迁移的接口契约参考。
|
||||
- SAE 部署时选 **JAR 包部署 + 标准 Java 应用运行环境 + Open JDK 17**,不要选 WAR/Tomcat/EDAS-Container。
|
||||
- 端口 `8080`,健康检查 `/actuator/health`,至少 2 实例。
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.6</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.juzi</groupId>
|
||||
<artifactId>taobao-mvp-backend</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>taobao-mvp-backend</name>
|
||||
<description>Taobao mini program + SAE + cloud gateway minimal MVP backend</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.juzi.mvp;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MvpApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MvpApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.juzi.mvp.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 淘宝云网关要求的统一响应结构。
|
||||
* 成功:{"success":true,"data":{...}}
|
||||
* 失败:{"success":false,"errorCode":"...","errorMessage":"..."}
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ApiResponse<T>(boolean success, T data, String errorCode, String errorMessage) {
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(true, data, null, null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String errorCode, String errorMessage) {
|
||||
return new ApiResponse<>(false, null, errorCode, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.juzi.mvp.api;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.juzi.mvp.gateway.TaobaoSignatureVerifier;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/taobao")
|
||||
public class MvpController {
|
||||
|
||||
private final TaobaoSignatureVerifier verifier;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MvpController(TaobaoSignatureVerifier verifier, ObjectMapper objectMapper) {
|
||||
this.verifier = verifier;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ping", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ApiResponse<Map<String, Object>> ping(@RequestBody(required = false) String rawBody,
|
||||
HttpServletRequest request) {
|
||||
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
|
||||
if (!check.accepted()) {
|
||||
return ApiResponse.error("SIGN_INVALID", check.message());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("ok", true);
|
||||
data.put("pong", true);
|
||||
data.put("server", "java");
|
||||
data.put("time", OffsetDateTime.now(ZoneId.of("Asia/Shanghai")).toString());
|
||||
data.put("transport", isGateway(request) ? "gateway" : "direct");
|
||||
data.put("appKey", param(request, "app_key"));
|
||||
data.put("openId", param(request, "open_id"));
|
||||
data.put("env", param(request, "env"));
|
||||
data.put("signVerified", check.verified());
|
||||
data.put("signMessage", check.message());
|
||||
return ApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/ping")
|
||||
public ApiResponse<Map<String, Object>> pingGet(HttpServletRequest request) {
|
||||
return ping(null, request);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/whoami", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ApiResponse<Map<String, Object>> whoami(@RequestBody(required = false) String rawBody,
|
||||
HttpServletRequest request) {
|
||||
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
|
||||
if (!check.accepted()) {
|
||||
return ApiResponse.error("SIGN_INVALID", check.message());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("ok", true);
|
||||
data.put("transport", isGateway(request) ? "gateway" : "direct");
|
||||
data.put("signVerified", check.verified());
|
||||
data.put("signMessage", check.message());
|
||||
data.put("params", sortedParams(request));
|
||||
return ApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/echo", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ApiResponse<Map<String, Object>> echo(@RequestBody(required = false) String rawBody,
|
||||
HttpServletRequest request) {
|
||||
TaobaoSignatureVerifier.Result check = verifier.verify(request, rawBody);
|
||||
if (!check.accepted()) {
|
||||
return ApiResponse.error("SIGN_INVALID", check.message());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("ok", true);
|
||||
data.put("bytes", rawBody == null ? 0 : rawBody.getBytes(StandardCharsets.UTF_8).length);
|
||||
data.put("body", parseBody(rawBody));
|
||||
data.put("signVerified", check.verified());
|
||||
return ApiResponse.ok(data);
|
||||
}
|
||||
|
||||
private Object parseBody(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(raw, new TypeReference<Object>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> fallback = new LinkedHashMap<>();
|
||||
fallback.put("_raw", raw);
|
||||
fallback.put("_parseError", e.getMessage());
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGateway(HttpServletRequest request) {
|
||||
return request.getParameter("app_key") != null;
|
||||
}
|
||||
|
||||
private String param(HttpServletRequest request, String name) {
|
||||
String value = request.getParameter(name);
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private Map<String, String> sortedParams(HttpServletRequest request) {
|
||||
Map<String, String> out = new TreeMap<>();
|
||||
request.getParameterMap().forEach((key, values) -> {
|
||||
if (values != null && values.length > 0 && !"sign".equals(key)) {
|
||||
out.put(key, values[0]);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.juzi.mvp.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 仅用于本地浏览器/curl 调试。小程序原生请求不受 CORS 限制。
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.juzi.mvp.gateway;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* 淘宝云网关签名校验的最小实现。
|
||||
*
|
||||
* 规则(与 h5/lib/taobao-auth.js 一致):
|
||||
* 1. 取全部 query 参数,去掉 sign 和空值,按 key 升序,拼成 key1value1key2value2...
|
||||
* 2. 末尾追加原始请求体(未经重新序列化的 JSON 字符串)
|
||||
* 3. sign_method=md5 时:MD5(secret + content + secret)
|
||||
* sign_method 为空时:MD5(content + secret)
|
||||
* hmac/hmac_md5:HMAC-MD5(secret, content)
|
||||
* hmac_sha256:HMAC-SHA256(secret, content)
|
||||
* 4. 结果转大写十六进制,与 sign 比较
|
||||
*
|
||||
* 本地直连没有 app_key/sign,默认 verify-sign=false 直接放行;
|
||||
* 云端部署时设置 TAOBAO_VERIFY_SIGN=true 和 TAOBAO_APP_KEY/TAOBAO_APP_SECRET。
|
||||
*/
|
||||
@Component
|
||||
public class TaobaoSignatureVerifier {
|
||||
|
||||
@Value("${taobao.app-key:}")
|
||||
private String appKey;
|
||||
|
||||
@Value("${taobao.app-secret:}")
|
||||
private String appSecret;
|
||||
|
||||
@Value("${taobao.verify-sign:false}")
|
||||
private boolean verifySign;
|
||||
|
||||
public record Result(boolean accepted, boolean verified, String message) {
|
||||
}
|
||||
|
||||
public Result verify(HttpServletRequest request, String rawBody) {
|
||||
if (!verifySign) {
|
||||
return new Result(true, false, "verify-sign=false, skipped (local/direct mode)");
|
||||
}
|
||||
if (appKey == null || appKey.isBlank() || appSecret == null || appSecret.isBlank()) {
|
||||
return new Result(false, false, "TAOBAO_APP_KEY / TAOBAO_APP_SECRET is not configured");
|
||||
}
|
||||
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
if (values == null || values.length == 0) {
|
||||
continue;
|
||||
}
|
||||
if (values.length > 1) {
|
||||
return new Result(false, false, "duplicate parameter: " + entry.getKey());
|
||||
}
|
||||
params.put(entry.getKey(), values[0]);
|
||||
}
|
||||
|
||||
if (!appKey.equals(params.get("app_key"))) {
|
||||
return new Result(false, false, "app_key mismatch");
|
||||
}
|
||||
String openId = params.getOrDefault("open_id", "");
|
||||
if (!openId.matches("[A-Za-z0-9_-]{4,256}")) {
|
||||
return new Result(false, false, "open_id missing or invalid");
|
||||
}
|
||||
String sign = params.getOrDefault("sign", "");
|
||||
if (!sign.matches("[A-Fa-f0-9]{32,64}")) {
|
||||
return new Result(false, false, "sign missing or invalid");
|
||||
}
|
||||
|
||||
try {
|
||||
String expected = sign(params, rawBody == null ? "" : rawBody, appSecret);
|
||||
if (expected == null || expected.length() != sign.length()) {
|
||||
return new Result(false, false, "unsupported sign_method");
|
||||
}
|
||||
boolean equal = MessageDigest.isEqual(
|
||||
expected.getBytes(StandardCharsets.UTF_8),
|
||||
sign.toUpperCase(Locale.ROOT).getBytes(StandardCharsets.UTF_8));
|
||||
return equal ? new Result(true, true, "ok") : new Result(false, false, "signature mismatch");
|
||||
} catch (Exception e) {
|
||||
return new Result(false, false, "signature error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static String sign(Map<String, String> params, String body, String secret) throws Exception {
|
||||
String method = params.getOrDefault("sign_method", "");
|
||||
if (!Set.of("", "md5", "hmac", "hmac_md5", "hmac_sha256").contains(method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder content = new StringBuilder();
|
||||
params.entrySet().stream()
|
||||
.filter(e -> !"sign".equals(e.getKey()))
|
||||
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.forEach(e -> content.append(e.getKey()).append(e.getValue()));
|
||||
content.append(body == null ? "" : body);
|
||||
|
||||
if ("hmac".equals(method) || "hmac_md5".equals(method) || "hmac_sha256".equals(method)) {
|
||||
String algorithm = "hmac_sha256".equals(method) ? "HmacSHA256" : "HmacMD5";
|
||||
Mac mac = Mac.getInstance(algorithm);
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm));
|
||||
return hex(mac.doFinal(content.toString().getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
String plain = ("md5".equals(method) ? secret : "") + content + secret;
|
||||
return hex(MessageDigest.getInstance("MD5").digest(plain.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
private static String hex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: taobao-mvp-backend
|
||||
jackson:
|
||||
time-zone: Asia/Shanghai
|
||||
default-property-inclusion: non_null
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
taobao:
|
||||
# 云端部署时通过 SAE 环境变量注入;本地留空即可。
|
||||
app-key: ${TAOBAO_APP_KEY:}
|
||||
app-secret: ${TAOBAO_APP_SECRET:}
|
||||
# 本地直连没有网关签名,保持 false;云端改成 true。
|
||||
verify-sign: ${TAOBAO_VERIFY_SIGN:false}
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.juzi.mvp: INFO
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.juzi.mvp;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class MvpApplicationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate rest;
|
||||
|
||||
private String url(String path) {
|
||||
return "http://127.0.0.1:" + port + path;
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void pingReturnsCloudGatewayEnvelope() {
|
||||
ResponseEntity<Map> response = rest.postForEntity(url("/api/taobao/ping"), Map.of(), Map.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).containsEntry("success", true);
|
||||
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
|
||||
assertThat(data).containsEntry("server", "java").containsEntry("pong", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void echoReturnsRequestBody() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
ResponseEntity<Map> response = rest.postForEntity(
|
||||
url("/api/taobao/echo"),
|
||||
new HttpEntity<>("{\"hello\":\"world\"}", headers),
|
||||
Map.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
|
||||
Map<String, Object> body = (Map<String, Object>) data.get("body");
|
||||
assertThat(body).containsEntry("hello", "world");
|
||||
}
|
||||
|
||||
@Test
|
||||
void actuatorHealthIsUp() {
|
||||
ResponseEntity<String> response = rest.getForEntity(url("/actuator/health"), String.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).contains("\"status\":\"UP\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.juzi.mvp.gateway;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TaobaoSignatureVerifierTest {
|
||||
|
||||
@Test
|
||||
void md5SignatureMatchesExistingNodeImplementation() throws Exception {
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("app_key", "app");
|
||||
params.put("open_id", "open-account");
|
||||
params.put("sign_method", "md5");
|
||||
params.put("env", "online");
|
||||
|
||||
String body = "{\"consent\":true,\"challenge\":\"nonce\"}";
|
||||
|
||||
// 由 taobao/test/runtime.test.cjs 的同一组参数计算得出。
|
||||
assertThat(TaobaoSignatureVerifier.sign(params, body, "secret"))
|
||||
.isEqualTo("E673310E672C9EB7E918857C6C64D247");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user