|
|
@@ -0,0 +1,588 @@
|
|
|
+<template>
|
|
|
+ <div class="help-content">
|
|
|
+ <div class="content-header">
|
|
|
+ <h2>统一认证平台 - 简易认证 (Simple Auth) 集成指南</h2>
|
|
|
+ <el-button type="primary" size="small" plain @click="downloadDoc('/docs/simple_auth.md', 'Simple_Auth_Guide.md')">
|
|
|
+ <el-icon style="margin-right: 5px"><Download /></el-icon>
|
|
|
+ 下载 开发文档
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <p class="intro">本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。</p>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>1. 核心流程图</h3>
|
|
|
+ <div class="flow-chart">
|
|
|
+ <div class="actor-row">
|
|
|
+ <div class="actor">用户 (User)</div>
|
|
|
+ <div class="actor">客户端 (Client)</div>
|
|
|
+ <div class="actor">统一认证平台 (UAP)</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow user-to-client">1. 输入账号/密码</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="self-action">2. 生成签名 (Sign)</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow client-to-uap">3. POST /login (账号+密码+签名)</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow uap-to-client dashed">4. 返回 Ticket (票据)</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="self-action">5. 内部逻辑处理</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow client-to-uap">6. POST /validate (Ticket+签名)</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow uap-to-client dashed">7. 返回 用户信息 (ID, Mobile...)</div>
|
|
|
+ </div>
|
|
|
+ <div class="step">
|
|
|
+ <div class="arrow client-to-user dashed">8. 登录成功</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>2. 前置准备</h3>
|
|
|
+ <p>在调用 API 之前,请确保您已在平台注册应用并获取以下信息:</p>
|
|
|
+ <ul>
|
|
|
+ <li><strong>App ID (<code>app_id</code>)</strong>: 应用唯一标识。</li>
|
|
|
+ <li><strong>App Secret (<code>app_secret</code>)</strong>: 应用密钥(<strong class="danger">严禁泄露给前端</strong>)。</li>
|
|
|
+ </ul>
|
|
|
+ <el-alert title="安全警告" type="error" :closable="false" show-icon class="alert-box">
|
|
|
+ <div>由于生成签名需要使用 <code>App Secret</code>,建议登录请求由您的<strong>应用后端</strong>发起,或者使用后端代理(BFF模式)。如果在前端(浏览器 JS)直接存储 Secret 并计算签名,极易导致密钥泄露。</div>
|
|
|
+ </el-alert>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>3. 签名算法 (Signature)</h3>
|
|
|
+ <p>所有涉及安全的接口都需要校验签名。</p>
|
|
|
+ <p><strong>签名生成步骤:</strong></p>
|
|
|
+ <ol>
|
|
|
+ <li><strong>准备参数</strong>:收集所有请求参数(不包括 <code>sign</code> 本身)。</li>
|
|
|
+ <li><strong>排序</strong>:按照参数名(key)的 ASCII 码从小到大排序。</li>
|
|
|
+ <li><strong>拼接</strong>:将排序后的参数拼接成 <code>key1=value1&key2=value2...</code> 格式的字符串。</li>
|
|
|
+ <li><strong>计算 HMAC</strong>:使用 <code>App Secret</code> 作为密钥,对拼接字符串进行 <strong>HMAC-SHA256</strong> 计算。</li>
|
|
|
+ <li><strong>转十六进制</strong>:将计算结果转换为 Hex 字符串即为签名。</li>
|
|
|
+ </ol>
|
|
|
+
|
|
|
+ <p><strong>Python 示例代码:</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+import hmac
|
|
|
+import hashlib
|
|
|
+import time
|
|
|
+
|
|
|
+def generate_signature(secret: str, params: dict) -> str:
|
|
|
+ # 1. 过滤掉空值和 sign 字段 (如果有)
|
|
|
+ data = {k: v for k, v in params.items() if k != "sign" and v is not None}
|
|
|
+
|
|
|
+ # 2. 排序 key
|
|
|
+ sorted_keys = sorted(data.keys())
|
|
|
+
|
|
|
+ # 3. 拼接字符串
|
|
|
+ query_string = "&".join([f"{k}={data[k]}" for k in sorted_keys])
|
|
|
+
|
|
|
+ # 4. 计算 HMAC-SHA256
|
|
|
+ signature = hmac.new(
|
|
|
+ secret.encode('utf-8'),
|
|
|
+ query_string.encode('utf-8'),
|
|
|
+ hashlib.sha256
|
|
|
+ ).hexdigest()
|
|
|
+
|
|
|
+ return signature
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>4. 接口开发详解</h3>
|
|
|
+
|
|
|
+ <h4>4.1 第一步:密码登录获取票据 (Login)</h4>
|
|
|
+ <p>用户在界面输入账号密码后,调用此接口获取临时票据(Ticket)。</p>
|
|
|
+ <ul>
|
|
|
+ <li><strong>接口地址</strong>: <code>POST /api/v1/simple/login</code></li>
|
|
|
+ <li><strong>Content-Type</strong>: <code>application/json</code></li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <p><strong>请求参数 (JSON Body):</strong></p>
|
|
|
+ <table class="param-table">
|
|
|
+ <thead>
|
|
|
+ <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
|
|
|
+ <tr><td><code>identifier</code></td><td>string</td><td>是</td><td>用户标识(手机号、用户名或邮箱)</td></tr>
|
|
|
+ <tr><td><code>password</code></td><td>string</td><td>是</td><td>用户明文密码</td></tr>
|
|
|
+ <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳(秒),有效期 300秒</td></tr>
|
|
|
+ <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名字符串</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <p><strong>请求示例:</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "app_id": "your_app_id",
|
|
|
+ "identifier": "13800138000",
|
|
|
+ "password": "user_password_123",
|
|
|
+ "timestamp": 1709876543,
|
|
|
+ "sign": "a1b2c3d4e5..."
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p><strong>响应成功 (200 OK):</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "ticket": "TICKET-7f8e9d0a-..."
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
+
|
|
|
+ <h4>4.2 (可选) 替代方案:验证码登录获取票据 (SMS Login)</h4>
|
|
|
+ <p>除了密码登录,您还可以使用手机验证码进行登录获取 Ticket。</p>
|
|
|
+ <ul>
|
|
|
+ <li><strong>接口地址</strong>: <code>POST /api/v1/simple/sms-login</code></li>
|
|
|
+ <li><strong>Content-Type</strong>: <code>application/json</code></li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <p><strong>请求参数 (JSON Body):</strong></p>
|
|
|
+ <table class="param-table">
|
|
|
+ <thead>
|
|
|
+ <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
|
|
|
+ <tr><td><code>mobile</code></td><td>string</td><td>是</td><td>用户手机号</td></tr>
|
|
|
+ <tr><td><code>code</code></td><td>string</td><td>是</td><td>短信验证码</td></tr>
|
|
|
+ <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳</td></tr>
|
|
|
+ <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名字符串</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <p><strong>注意:</strong> 在调用此接口前,需先调用 <code>/api/v1/auth/sms/send-code</code> 发送验证码。</p>
|
|
|
+
|
|
|
+ <p><strong>请求示例:</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "app_id": "your_app_id",
|
|
|
+ "mobile": "13800138000",
|
|
|
+ "code": "123456",
|
|
|
+ "timestamp": 1709876543,
|
|
|
+ "sign": "a1b2c3d4e5..."
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p><strong>响应成功 (200 OK):</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "ticket": "TICKET-7f8e9d0a-..."
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
+
|
|
|
+ <h4>4.3 第二步:验证票据并获取用户信息 (Validate)</h4>
|
|
|
+ <p>拿到 <code>ticket</code> 后,立即调用此接口解析出用户身份。</p>
|
|
|
+ <ul>
|
|
|
+ <li><strong>接口地址</strong>: <code>POST /api/v1/simple/validate</code></li>
|
|
|
+ <li><strong>Content-Type</strong>: <code>application/json</code></li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <p><strong>请求参数 (JSON Body):</strong></p>
|
|
|
+ <table class="param-table">
|
|
|
+ <thead>
|
|
|
+ <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
|
|
|
+ <tr><td><code>ticket</code></td><td>string</td><td>是</td><td>上一步获取到的票据</td></tr>
|
|
|
+ <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳</td></tr>
|
|
|
+ <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名(注意参数变化,需重新计算)</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <p><strong>请求示例:</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "app_id": "your_app_id",
|
|
|
+ "ticket": "TICKET-7f8e9d0a-...",
|
|
|
+ "timestamp": 1709876545,
|
|
|
+ "sign": "f9e8d7c6b5..."
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p><strong>响应成功 (200 OK):</strong></p>
|
|
|
+ <p>此接口返回 <code>valid: true</code> 表示票据有效,并附带用户数据。</p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "valid": true,
|
|
|
+ "user_id": 1001,
|
|
|
+ "mobile": "13800138000",
|
|
|
+ "mapped_key": "user_zhangsan", // 第三方映射ID(如果有)
|
|
|
+ "mapped_email": "zhangsan@example.com" // 映射邮箱(如果有)
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p><strong>响应失败 (票据无效或过期):</strong></p>
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+{
|
|
|
+ "valid": false
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section">
|
|
|
+ <h3>5. 多语言调用示例</h3>
|
|
|
+ <p>以下提供了多种编程语言计算签名并发起请求的示例代码。</p>
|
|
|
+
|
|
|
+ <el-tabs type="border-card" class="code-tabs">
|
|
|
+ <el-tab-pane label="Python">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+import requests
|
|
|
+import time
|
|
|
+import hmac
|
|
|
+import hashlib
|
|
|
+import json
|
|
|
+
|
|
|
+# 配置信息
|
|
|
+API_BASE = "http://localhost:8000/api/v1/simple"
|
|
|
+APP_ID = "test_app_001"
|
|
|
+APP_SECRET = "secret_key_abc123" # 务必保密
|
|
|
+
|
|
|
+def get_sign(params):
|
|
|
+ # 排除 sign 字段
|
|
|
+ data = {k: v for k, v in params.items() if k != "sign"}
|
|
|
+ # 排序并拼接
|
|
|
+ query_string = "&".join([f"{k}={data[k]}" for k in sorted(data.keys())])
|
|
|
+ # HMAC-SHA256
|
|
|
+ return hmac.new(APP_SECRET.encode(), query_string.encode(), hashlib.sha256).hexdigest()
|
|
|
+
|
|
|
+def main():
|
|
|
+ # === 步骤 1: 登录获取 Ticket ===
|
|
|
+ login_ts = int(time.time())
|
|
|
+ login_payload = {
|
|
|
+ "app_id": APP_ID,
|
|
|
+ "identifier": "13800000001",
|
|
|
+ "password": "password123",
|
|
|
+ "timestamp": login_ts
|
|
|
+ }
|
|
|
+ # 计算签名
|
|
|
+ login_payload["sign"] = get_sign(login_payload)
|
|
|
+
|
|
|
+ print(f"1. 正在尝试登录: {login_payload['identifier']} ...")
|
|
|
+ resp = requests.post(f"{API_BASE}/login", json=login_payload)
|
|
|
+
|
|
|
+ if resp.status_code != 200:
|
|
|
+ print(f"登录失败: {resp.text}")
|
|
|
+ return
|
|
|
+
|
|
|
+ ticket = resp.json().get("ticket")
|
|
|
+ print(f"登录成功! 获取到 Ticket: {ticket}")
|
|
|
+
|
|
|
+ # === 步骤 2: 使用 Ticket 换取用户信息 ===
|
|
|
+ validate_ts = int(time.time())
|
|
|
+ validate_payload = {
|
|
|
+ "app_id": APP_ID,
|
|
|
+ "ticket": ticket,
|
|
|
+ "timestamp": validate_ts
|
|
|
+ }
|
|
|
+ # 重新计算签名(参数变了,签名必须重算)
|
|
|
+ validate_payload["sign"] = get_sign(validate_payload)
|
|
|
+
|
|
|
+ print(f"\n2. 正在验证 Ticket...")
|
|
|
+ v_resp = requests.post(f"{API_BASE}/validate", json=validate_payload)
|
|
|
+
|
|
|
+ user_info = v_resp.json()
|
|
|
+ if user_info.get("valid"):
|
|
|
+ print("验证成功! 用户信息如下:")
|
|
|
+ print(json.dumps(user_info, indent=2, ensure_ascii=False))
|
|
|
+ else:
|
|
|
+ print("Ticket 无效或已过期")
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="Java">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.util.*;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+
|
|
|
+public class AuthExample {
|
|
|
+ private static final String SECRET = "secret_key_abc123";
|
|
|
+
|
|
|
+ public static String generateSign(Map<String, String> params) {
|
|
|
+ try {
|
|
|
+ // 1. 排序
|
|
|
+ List<String> sortedKeys = new ArrayList<>(params.keySet());
|
|
|
+ Collections.sort(sortedKeys);
|
|
|
+
|
|
|
+ // 2. 拼接
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (String key : sortedKeys) {
|
|
|
+ if (!key.equals("sign") && params.get(key) != null) {
|
|
|
+ if (sb.length() > 0) sb.append("&");
|
|
|
+ sb.append(key).append("=").append(params.get(key));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. HMAC-SHA256
|
|
|
+ Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
|
|
+ SecretKeySpec secret_key = new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
|
|
+ sha256_HMAC.init(secret_key);
|
|
|
+
|
|
|
+ byte[] bytes = sha256_HMAC.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ // 4. Hex
|
|
|
+ StringBuilder hex = new StringBuilder();
|
|
|
+ for (byte b : bytes) {
|
|
|
+ hex.append(String.format("%02x", b));
|
|
|
+ }
|
|
|
+ return hex.toString();
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void main(String[] args) {
|
|
|
+ Map<String, String> params = new HashMap<>();
|
|
|
+ params.put("app_id", "test_app_001");
|
|
|
+ params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
|
|
|
+
|
|
|
+ System.out.println("Sign: " + generateSign(params));
|
|
|
+ }
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="Android (Kotlin)">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+import javax.crypto.Mac
|
|
|
+import javax.crypto.spec.SecretKeySpec
|
|
|
+
|
|
|
+object AuthUtils {
|
|
|
+ private const val APP_SECRET = "secret_key_abc123"
|
|
|
+
|
|
|
+ fun generateSign(params: Map<String, String>): String {
|
|
|
+ // 1. 过滤 & 排序
|
|
|
+ val sortedKeys = params.keys.filter { it != "sign" }.sorted()
|
|
|
+
|
|
|
+ // 2. 拼接
|
|
|
+ val queryString = sortedKeys.joinToString("&") { key ->
|
|
|
+ "$key=${params[key]}"
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. HMAC-SHA256
|
|
|
+ val hmacSha256 = "HmacSHA256"
|
|
|
+ val secretKeySpec = SecretKeySpec(APP_SECRET.toByteArray(Charsets.UTF_8), hmacSha256)
|
|
|
+ val mac = Mac.getInstance(hmacSha256)
|
|
|
+ mac.init(secretKeySpec)
|
|
|
+
|
|
|
+ val bytes = mac.doFinal(queryString.toByteArray(Charsets.UTF_8))
|
|
|
+
|
|
|
+ // 4. Hex
|
|
|
+ return bytes.joinToString("") { "%02x".format(it) }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Usage Example
|
|
|
+fun main() {
|
|
|
+ val params = mapOf(
|
|
|
+ "app_id" to "test_app_001",
|
|
|
+ "identifier" to "13800000001",
|
|
|
+ "timestamp" to (System.currentTimeMillis() / 1000).toString()
|
|
|
+ )
|
|
|
+ val sign = AuthUtils.generateSign(params)
|
|
|
+ println("Signature: $sign")
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="JavaScript (Node.js)">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+const crypto = require('crypto');
|
|
|
+const axios = require('axios'); // npm install axios
|
|
|
+
|
|
|
+const APP_ID = 'test_app_001';
|
|
|
+const APP_SECRET = 'secret_key_abc123';
|
|
|
+const BASE_URL = 'http://localhost:8000/api/v1/simple';
|
|
|
+
|
|
|
+function getSign(params) {
|
|
|
+ // 1. 过滤 & 排序
|
|
|
+ const keys = Object.keys(params)
|
|
|
+ .filter(k => k !== 'sign' && params[k] !== undefined)
|
|
|
+ .sort();
|
|
|
+
|
|
|
+ // 2. 拼接 Query String
|
|
|
+ const queryString = keys.map(k => `${k}=${params[k]}`).join('&');
|
|
|
+
|
|
|
+ // 3. HMAC-SHA256
|
|
|
+ return crypto.createHmac('sha256', APP_SECRET)
|
|
|
+ .update(queryString)
|
|
|
+ .digest('hex');
|
|
|
+}
|
|
|
+
|
|
|
+async function login() {
|
|
|
+ const timestamp = Math.floor(Date.now() / 1000);
|
|
|
+ const payload = {
|
|
|
+ app_id: APP_ID,
|
|
|
+ identifier: '13800000001',
|
|
|
+ password: 'password123',
|
|
|
+ timestamp: timestamp
|
|
|
+ };
|
|
|
+
|
|
|
+ payload.sign = getSign(payload);
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('Sending login request...');
|
|
|
+ const res = await axios.post(`${BASE_URL}/login`, payload);
|
|
|
+ console.log('Ticket:', res.data.ticket);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Login Failed:', error.response?.data || error.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+login();
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="Go">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "crypto/hmac"
|
|
|
+ "crypto/sha256"
|
|
|
+ "encoding/hex"
|
|
|
+ "fmt"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+func GetSign(secret string, params map[string]interface{}) string {
|
|
|
+ // 1. 提取 Key
|
|
|
+ var keys []string
|
|
|
+ for k := range params {
|
|
|
+ if k != "sign" {
|
|
|
+ keys = append(keys, k)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 2. 排序
|
|
|
+ sort.Strings(keys)
|
|
|
+
|
|
|
+ // 3. 拼接
|
|
|
+ var parts []string
|
|
|
+ for _, k := range keys {
|
|
|
+ val := fmt.Sprintf("%v", params[k])
|
|
|
+ parts = append(parts, fmt.Sprintf("%s=%s", k, val))
|
|
|
+ }
|
|
|
+ query := strings.Join(parts, "&")
|
|
|
+
|
|
|
+ // 4. HMAC-SHA256
|
|
|
+ h := hmac.New(sha256.New, []byte(secret))
|
|
|
+ h.Write([]byte(query))
|
|
|
+ return hex.EncodeToString(h.Sum(nil))
|
|
|
+}
|
|
|
+
|
|
|
+func main() {
|
|
|
+ params := map[string]interface{}{
|
|
|
+ "app_id": "test_app_001",
|
|
|
+ "identifier": "13800000001",
|
|
|
+ "password": "123456",
|
|
|
+ "timestamp": time.Now().Unix(),
|
|
|
+ }
|
|
|
+
|
|
|
+ secret := "secret_key_abc123"
|
|
|
+ sign := GetSign(secret, params)
|
|
|
+ fmt.Printf("Signature: %s\n", sign)
|
|
|
+}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="Swift">
|
|
|
+ <div class="code-block">
|
|
|
+ <pre>
|
|
|
+import Foundation
|
|
|
+import CommonCrypto
|
|
|
+
|
|
|
+// 注意:需要添加 Bridging Header 引入 CommonCrypto 或直接在 Linux 环境使用
|
|
|
+
|
|
|
+func hmac(string: String, key: String) -> String {
|
|
|
+ var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
|
+ CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), key, key.count, string, string.count, &digest)
|
|
|
+ let data = Data(digest)
|
|
|
+ return data.map { String(format: "%02hhx", $0) }.joined()
|
|
|
+}
|
|
|
+
|
|
|
+func generateSign(secret: String, params: [String: Any]) -> String {
|
|
|
+ // 1. 过滤 & 排序
|
|
|
+ let sortedKeys = params.keys.filter { $0 != "sign" }.sorted()
|
|
|
+
|
|
|
+ // 2. 拼接
|
|
|
+ let queryParts = sortedKeys.map { key in
|
|
|
+ return "\(key)=\(params[key]!)"
|
|
|
+ }
|
|
|
+ let queryString = queryParts.joined(separator: "&")
|
|
|
+
|
|
|
+ // 3. HMAC
|
|
|
+ return hmac(string: queryString, key: secret)
|
|
|
+}
|
|
|
+
|
|
|
+// Usage
|
|
|
+let params: [String: Any] = [
|
|
|
+ "app_id": "test_app_001",
|
|
|
+ "timestamp": Int(Date().timeIntervalSince1970)
|
|
|
+]
|
|
|
+let sign = generateSign(secret: "secret_123", params: params)
|
|
|
+print(sign)
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { Download } from '@element-plus/icons-vue'
|
|
|
+import { useHelpDocs } from '../../composables/useHelpDocs'
|
|
|
+
|
|
|
+const { downloadDoc } = useHelpDocs()
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+@import './help.css';
|
|
|
+</style>
|