# 统一认证平台 - 简易认证 (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 (可选) 验证码登录 (SMS Login)
使用短信验证码获取票据 (Ticket)。

- **URL**: `POST /sms-login`
- **Content-Type**: `application/json`

**Request Body**:
| Field | Type | Required | Description |
|---|---|---|---|
| `app_id` | string | Yes | 应用 ID |
| `mobile` | string | Yes | 用户手机号 |
| `code` | string | Yes | 短信验证码 |
| `timestamp` | int | Yes | 当前时间戳 |
| `sign` | string | Yes | 签名 |

**Response (200)**:
```json
{
  "ticket": "TICKET-7f8e9d0a-..." 
}
```

### 5.3 验证票据 (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 generate_signature(secret: str, params: dict) -> str:
    # 排除 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(secret.encode('utf-8'), query_string.encode('utf-8'), 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"] = generate_signature(APP_SECRET, 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"] = generate_signature(APP_SECRET, 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
// 依赖建议: OkHttp 或 Apache HttpClient, FastJson/Jackson
import okhttp3.*;
import com.alibaba.fastjson.JSON;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class AuthExample {
    private static final String APP_ID = "test_app_001";
    private static final String APP_SECRET = "secret_key_abc123";
    private static final String API_BASE = "{{API_BASE_URL}}/simple";

    public static String generateSign(Map<String, String> params, String secret) {
        try {
            List<String> sortedKeys = new ArrayList<>(params.keySet());
            Collections.sort(sortedKeys);
            
            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));
                }
            }
            
            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));
            StringBuilder hex = new StringBuilder();
            for (byte b : bytes) hex.append(String.format("%02x", b));
            return hex.toString();
        } catch (Exception e) {
            return "";
        }
    }
    
    public static void main(String[] args) {
        // 示例：仅生成签名
        Map<String, String> params = new HashMap<>();
        params.put("app_id", APP_ID);
        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        
        System.out.println("Sign: " + generateSign(params, APP_SECRET));
    }
}
```

### Android (Kotlin)
```kotlin
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")
}
```

### 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();
```

### Go
```go
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)
}
```

### Swift
```swift
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)
```
