simple_auth.md 12 KB

统一认证平台 - 简易认证 (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 示例

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):

{
  "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):

{
  "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):

{
  "valid": true,
  "user_id": 1001,
  "mobile": "13800138000",
  "mapped_key": "user_zhangsan",  // 第三方映射ID
  "mapped_email": "zhangsan@example.com"
}

Response (Invalid):

{ "valid": false }

6. 多语言调用示例

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

// 依赖建议: 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)

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)

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

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

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)