Simple_Auth_Guide.md 16 KB

统一认证平台 - 简易认证 (Simple Auth) 集成指南

本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。


1. 核心流程图

sequenceDiagram
    participant User as 用户
    participant Client as 您的应用(前端/后端)
    participant UAP as 统一认证平台

    User->>Client: 输入账号/密码
    Client->>Client: 生成签名 (Sign)
    Client->>UAP: POST /api/v1/simple/login (账号+密码+签名)
    UAP-->>Client: 返回 Ticket (票据)
    Client->>Client: (内部逻辑处理)
    Client->>UAP: POST /api/v1/simple/validate (Ticket+签名)
    UAP-->>Client: 返回 用户信息 (ID, Mobile, etc.)
    Client->>User: 登录成功

2. 前置准备

在调用 API 之前,请确保您已在平台注册应用并获取以下信息:

  • App ID (app_id): 应用唯一标识。
  • App Secret (app_secret): 应用密钥(严禁泄露给前端)。
  • Access Token (access_token): 应用访问令牌,用于后端 M2M 同步接口鉴权(严禁泄露给前端)。

⚠️ 安全警告:由于生成签名需要使用 App Secret,建议登录请求由您的应用后端发起,或者使用后端代理(BFF模式)。如果在前端(浏览器 JS)直接存储 Secret 并计算签名,极易导致密钥泄露。


3. 签名算法 (Signature)

所有涉及安全的接口都需要校验签名。

签名生成步骤:

  1. 准备参数:收集所有请求参数(不包括 sign 本身)。
  2. 排序:按照参数名(key)的 ASCII 码从小到大排序。
  3. 拼接:将排序后的参数拼接成 key1=value1&key2=value2... 格式的字符串。
  4. 计算 HMAC:使用 App Secret 作为密钥,对拼接字符串进行 HMAC-SHA256 计算。
  5. 转十六进制:将计算结果转换为 Hex 字符串即为签名。

Python 示例代码:

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

4. 接口开发详解

4.1 第一步:密码登录获取票据 (Login)

用户在界面输入账号密码后,调用此接口获取临时票据(Ticket)。

  • 接口地址: POST /api/v1/simple/login
  • Content-Type: application/json

请求参数 (JSON Body):

字段 类型 必填 说明
app_id string 您的应用 ID
identifier string 用户标识(手机号、用户名或邮箱)
password string 用户明文密码
timestamp int 当前时间戳(秒),有效期 300秒
sign string 签名字符串

请求示例:

{
  "app_id": "your_app_id",
  "identifier": "13800138000",
  "password": "user_password_123",
  "timestamp": 1709876543,
  "sign": "a1b2c3d4e5..." 
}

响应成功 (200 OK):

{
  "ticket": "TICKET-7f8e9d0a-..." 
}

4.2 第二步:验证票据并获取用户信息 (Validate)

拿到 ticket 后,立即调用此接口解析出用户身份。

  • 接口地址: POST /api/v1/simple/validate
  • Content-Type: application/json

请求参数 (JSON Body):

字段 类型 必填 说明
app_id string 您的应用 ID
ticket string 上一步获取到的票据
timestamp int 当前时间戳
sign string 签名(注意参数变化,需重新计算)

请求示例:

{
  "app_id": "your_app_id",
  "ticket": "TICKET-7f8e9d0a-...",
  "timestamp": 1709876545,
  "sign": "f9e8d7c6b5..."
}

响应成功 (200 OK):

此接口返回 valid: true 表示票据有效,并附带用户数据。

{
  "valid": true,
  "user_id": 1001,
  "mobile": "13800138000",
  "mapped_key": "user_zhangsan",  // 第三方映射ID(如果有)
  "mapped_email": "zhangsan@example.com" // 映射邮箱(如果有)
}

响应失败 (票据无效或过期):

{
  "valid": false
}

4.3 票据交换 (Ticket Exchange) [跨应用免登]

当用户已在 源应用 (Source App) 登录,需要无缝跳转到 目标应用 (Target App) 且实现免登录时,使用此接口。

  • 场景: 用户在 App A 点击 "跳转到 App B",App A 后端调用此接口获取 App B 的票据,然后将用户重定向到 App B。
  • 接口地址: POST /api/v1/simple/exchange
  • Content-Type: application/json

请求参数 (JSON Body):

字段 类型 必填 说明
app_id string 源应用 ID (发起方)
target_app_id string 目标应用 ID (跳转方)
user_mobile string 用户手机号 (作为用户身份标识)
timestamp int 当前时间戳
sign string 签名 (使用源应用的 Secret 计算)

请求示例:

{
  "app_id": "source_app_A",
  "target_app_id": "target_app_B",
  "user_mobile": "13800138000",
  "timestamp": 1709876545,
  "sign": "generated_signature_hex"
}

响应成功 (200 OK):

{
  "ticket": "TICKET-for-target-app-...",
  "redirect_url": "http://target-app-b.com/sso/callback?ticket=TICKET-..."
}
  • ticket: 专门为目标应用生成的登录票据。
  • redirect_url: 拼接了 ticket 的目标应用重定向地址(取自目标应用配置的 redirect_uris 的第一个地址)。

后续流程:

  1. 源应用前端接收到 redirect_url
  2. 浏览器跳转到 redirect_url
  3. 目标应用后端接收请求,提取 ticket
  4. 目标应用后端调用 validate 接口(注意:此时 app_id 为目标应用 ID,sign 使用目标应用 Secret)来验证票据。

4.4 接口对比:/login vs /sso-login

平台提供了两个登录接口,它们在功能上有一定重叠,但设计目的和使用场景不同

4.4.1 /login 接口(通用登录)

适用场景:

  • ✅ 后端服务器直接调用(推荐使用签名验证)
  • ✅ 需要签名验证保证安全性的场景
  • ✅ 支持所有类型的应用(SIMPLE_API / OIDC)
  • ✅ 既支持平台登录,也支持应用 SSO 登录

接口特点:

  • 双模式设计:根据是否提供 app_id 返回不同结果
    • 不提供 app_id:返回平台 access_token(用于访问平台管理功能)
    • 提供 app_id:返回应用 ticket(用于应用 SSO 登录)
  • 支持签名验证signtimestamp 可选,但推荐使用
  • 返回原始票据:只返回 ticket,需要调用方自己构建跳转 URL

请求参数:

字段 必填 说明
app_id ❌ 选填 不提供时为平台登录,提供时为应用 SSO 登录
identifier 必填 用户标识:手机号、映射key或映射email
password 必填 用户密码
sign ❌ 选填 签名(如果提供,必须同时提供timestamp)
timestamp ❌ 选填 时间戳(如果提供,必须同时提供sign)

请求示例(应用 SSO 登录):

{
  "app_id": "your_app_id",
  "identifier": "13800138000",
  "password": "user_password_123",
  "timestamp": 1709876543,
  "sign": "a1b2c3d4e5..." 
}

响应示例(应用 SSO 登录):

{
  "ticket": "TICKET-7f8e9d0a-..."
}

请求示例(平台登录):

{
  "identifier": "13800138000",
  "password": "admin_password_123"
}

响应示例(平台登录):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "role": "SUPER_ADMIN"
}

4.4.2 /sso-login 接口(简易 SSO 登录)

适用场景:

  • ✅ 前端浏览器调用(简化跳转流程)
  • ✅ 支持基于会话的单点登录(用户已在平台登录)
  • ✅ 快速实现跨应用免登录
  • ⚠️ 仅支持 SIMPLE_API 类型的应用(OIDC 应用请使用标准流程)

接口特点:

  • 双认证模式
    1. 会话认证优先:如果用户已登录 UAP 平台(携带有效 token),自动使用当前用户身份
    2. 凭据认证备用:如果未登录,可以提供 usernamepassword 进行认证
  • 直接返回跳转 URL:返回完整的 redirect_url,前端直接跳转即可
  • 无需签名:简化调用流程,适合前端使用
  • 应用类型限制:只支持 SIMPLE_API 应用

请求参数:

字段 必填 说明
app_id 必填 应用ID(只支持 SIMPLE_API 类型)
username ⚠️ 条件必填 用户名(如果用户未登录UAP,则必须提供)
password ⚠️ 条件必填 密码(如果用户未登录UAP,则必须提供)

使用场景1:用户已在 UAP 登录(会话认证)

前端携带平台 token(通过 Authorization 头或 Cookie)调用接口:

{
  "app_id": "my_app_123"
}

响应:

{
  "redirect_url": "https://your-app.com/callback?ticket=TICKET-xxx"
}

前端直接跳转:

// 用户已登录,自动 SSO
const response = await fetch('/api/v1/simple/sso-login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + localStorage.getItem('access_token')
  },
  body: JSON.stringify({ app_id: 'my_app_123' })
});

const { redirect_url } = await response.json();
window.location.href = redirect_url;  // 直接跳转

使用场景2:用户未登录(凭据认证)

前端提供用户名和密码:

{
  "app_id": "my_app_123",
  "username": "13800138000",
  "password": "password123"
}

响应:

{
  "redirect_url": "https://your-app.com/callback?ticket=TICKET-xxx"
}

前端直接跳转:

// 用户未登录,使用凭据登录
const response = await fetch('/api/v1/simple/sso-login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    app_id: 'my_app_123',
    username: '13800138000',
    password: 'password123'
  })
});

const { redirect_url } = await response.json();
window.location.href = redirect_url;  // 直接跳转

4.4.3 选择建议

使用场景 推荐接口 原因
后端服务器调用 /login 支持签名验证,安全性更高
前端浏览器跳转 /sso-login 直接返回跳转 URL,简化流程
单点登录(已登录用户) /sso-login 支持会话认证,无需再次输入密码
OIDC 应用 /login 或标准 OIDC 流程 /sso-login 不支持 OIDC
需要平台管理功能 /login(不提供 app_id) 获取平台 access_token
跨应用免登录 /sso-login/exchange 取决于是否需要服务端签名

核心区别总结:

特性 /login /sso-login
认证方式 只支持用户名+密码 支持会话认证 OR 用户名+密码
签名验证 ✅ 支持(可选) ❌ 不支持
应用类型限制 ❌ 无限制 ✅ 只支持 SIMPLE_API
返回值 ticketaccess_token redirect_url(含 ticket)
会话支持 ❌ 不支持 ✅ 支持(优先使用)
适用端 后端服务器 前端浏览器
使用复杂度 中等(需要自己构建跳转) 简单(直接跳转)

4.5 账号同步 (M2M)

此接口用于将外部业务系统(如 OA、CRM)的用户账号关系同步到本平台。支持批量调用,实现“本平台用户(手机号)”与“外部应用账号(ID/邮箱)”的绑定。

  • 接口地址: POST /api/v1/apps/mapping/sync
  • 认证方式: 请求头需包含 X-App-Access-Token(可在应用详情页查看)。

接口逻辑:

  1. 根据 mobile 查找用户。如果用户不存在,自动创建新用户(生成随机密码,默认激活)。
  2. 将该用户与当前应用建立映射关系(绑定 mapped_keymapped_email)。
  3. 如果映射已存在,则更新映射信息。

请求参数 (JSON Body):

字段 类型 必填 说明
mobile string 用户手机号(平台唯一标识)
mapped_key string 外部系统中的用户ID(在该应用下唯一)
mapped_email string 外部系统中的邮箱(在该应用下唯一)
is_active boolean 映射关系状态(true启用,false禁用)
password string (忽略)仅用于占位,不会更新用户密码
status string (忽略)仅用于占位

请求示例:

curl -X POST "http://your-uap-domain/api/v1/apps/mapping/sync" \
     -H "Content-Type: application/json" \
     -H "X-App-Access-Token: YOUR_APP_ACCESS_TOKEN" \
     -d '{
           "mobile": "13800138000",
           "mapped_key": "user_1001",
           "mapped_email": "zhangsan@example.com",
           "is_active": true
         }'

响应成功 (200 OK):

{
  "id": 123,
  "app_id": 1,
  "user_id": 456,
  "mapped_key": "user_1001",
  "mapped_email": "zhangsan@example.com",
  "user_mobile": "13800138000",
  "user_status": "ACTIVE",
  "is_active": true
}

响应失败:

  • 400 Bad Request: mapped_keymapped_email 已被其他用户占用。
  • 403 Forbidden: Access Token 无效或过期。

5. 完整调用示例 (Python)

这是一个模拟客户端的完整脚本,演示如何登录并获取数据:

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