simple_auth.md 7.7 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 验证票据 (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 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

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)

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