|
|
@@ -0,0 +1,267 @@
|
|
|
+# 统一认证平台 - 简易认证 (Simple Auth) 集成指南
|
|
|
+
|
|
|
+## 1. 概述
|
|
|
+本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。
|
|
|
+
|
|
|
+**Base URL**: `{{API_BASE_URL}}/simple` (请根据实际部署环境替换)
|
|
|
+
|
|
|
+## 2. 核心流程
|
|
|
+1. **用户输入**: 用户在客户端输入账号密码。
|
|
|
+2. **签名**: 客户端/后端生成签名 (Sign)。
|
|
|
+3. **登录**: POST `/login` (账号+密码+签名) -> 获取 `Ticket`。
|
|
|
+4. **验证**: POST `/validate` (Ticket+签名) -> 获取用户信息。
|
|
|
+
|
|
|
+## 3. 安全警告
|
|
|
+- **App Secret** 严禁泄露给前端浏览器。
|
|
|
+- 建议所有涉及 Secret 的签名计算都在**后端**完成。
|
|
|
+
|
|
|
+## 4. 签名算法 (Signature)
|
|
|
+所有接口(除部分公开接口外)都需要校验签名。
|
|
|
+
|
|
|
+**步骤**:
|
|
|
+1. **准备参数**: 收集所有请求参数(**排除 `sign` 本身**)。
|
|
|
+2. **排序**: 按照参数名(key)的 ASCII 码从小到大排序。
|
|
|
+3. **拼接**: 将排序后的参数拼接成 `key1=value1&key2=value2...` 格式。
|
|
|
+4. **计算 HMAC**: 使用 `App Secret` 作为密钥,对拼接字符串进行 **HMAC-SHA256** 计算。
|
|
|
+5. **Hex 编码**: 将结果转换为十六进制字符串。
|
|
|
+
|
|
|
+### Python 示例
|
|
|
+```python
|
|
|
+import hmac
|
|
|
+import hashlib
|
|
|
+
|
|
|
+def generate_signature(secret: str, params: dict) -> str:
|
|
|
+ data = {k: v for k, v in params.items() if k != "sign" and v is not None}
|
|
|
+ sorted_keys = sorted(data.keys())
|
|
|
+ query_string = "&".join([f"{k}={data[k]}" for k in sorted_keys])
|
|
|
+ signature = hmac.new(
|
|
|
+ secret.encode('utf-8'),
|
|
|
+ query_string.encode('utf-8'),
|
|
|
+ hashlib.sha256
|
|
|
+ ).hexdigest()
|
|
|
+ return signature
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 接口定义
|
|
|
+
|
|
|
+### 5.1 密码登录 (Login)
|
|
|
+获取临时票据 (Ticket)。
|
|
|
+
|
|
|
+- **URL**: `POST /login`
|
|
|
+- **Content-Type**: `application/json`
|
|
|
+
|
|
|
+**Request Body**:
|
|
|
+| Field | Type | Required | Description |
|
|
|
+|---|---|---|---|
|
|
|
+| `app_id` | string | Yes | 应用 ID |
|
|
|
+| `identifier` | string | Yes | 用户标识(手机号、用户名或邮箱) |
|
|
|
+| `password` | string | Yes | 明文密码 |
|
|
|
+| `timestamp` | int | Yes | 当前时间戳 (秒) |
|
|
|
+| `sign` | string | Yes | 签名 |
|
|
|
+
|
|
|
+**Response (200)**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "ticket": "TICKET-7f8e9d0a-..."
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 验证票据 (Validate)
|
|
|
+解析票据获取用户信息。
|
|
|
+
|
|
|
+- **URL**: `POST /validate`
|
|
|
+- **Content-Type**: `application/json`
|
|
|
+
|
|
|
+**Request Body**:
|
|
|
+| Field | Type | Required | Description |
|
|
|
+|---|---|---|---|
|
|
|
+| `app_id` | string | Yes | 应用 ID |
|
|
|
+| `ticket` | string | Yes | 上一步获取的票据 |
|
|
|
+| `timestamp` | int | Yes | 当前时间戳 |
|
|
|
+| `sign` | string | Yes | 签名 (参数变化需重新计算) |
|
|
|
+
|
|
|
+**Response (200)**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "valid": true,
|
|
|
+ "user_id": 1001,
|
|
|
+ "mobile": "13800138000",
|
|
|
+ "mapped_key": "user_zhangsan", // 第三方映射ID
|
|
|
+ "mapped_email": "zhangsan@example.com"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Response (Invalid)**:
|
|
|
+```json
|
|
|
+{ "valid": false }
|
|
|
+```
|
|
|
+
|
|
|
+## 6. 多语言调用示例
|
|
|
+
|
|
|
+### Python
|
|
|
+```python
|
|
|
+import requests
|
|
|
+import time
|
|
|
+import hmac
|
|
|
+import hashlib
|
|
|
+import json
|
|
|
+
|
|
|
+# 配置信息
|
|
|
+API_BASE = "{{API_BASE_URL}}/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()
|
|
|
+```
|
|
|
+
|
|
|
+### Java
|
|
|
+```java
|
|
|
+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));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### JavaScript (Node.js)
|
|
|
+```javascript
|
|
|
+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 = '{{API_BASE_URL}}/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();
|
|
|
+```
|