# 快速对接指南 (Redirect SSO) ## 1. 概述 这是最简单、最快速的集成方式。您无需开发前端登录页面,只需将用户重定向到统一认证平台,待用户登录后,平台会将携带票据 (Ticket) 的用户重定向回您的系统。 ## 2. 核心流程 1. **用户访问**: 用户访问您的应用 (未登录)。 2. **跳转登录**: 应用重定向用户到 UAP 登录页面 (`?app_id=xxx`)。 3. **用户登录**: 用户在 UAP 完成认证。 4. **回调应用**: UAP 重定向回您的应用回调地址 (`?ticket=xxx`)。 5. **验证票据**: 应用后端调用接口验证 Ticket。 6. **登录成功**: 验证通过,应用创建自身会话。 ## 3. 详细集成步骤 ### 第一步:准备工作 - 在 UAP 管理后台创建应用。 - **关键**:在应用配置中填写入合法的 **回调地址 (Redirect URIs)**。例如:`http://your-app.com/callback` - 获取 `App ID` 和 `App Secret` (用于后端验证 Ticket)。 ### 第二步:拼接登录链接 (前端) 在您的应用中,检测到用户未登录时,直接跳转到以下地址: **PC 端**: `https://api.hnyunzhu.com/api/v1/login?app_id=YOUR_APP_ID` **移动端 (H5)**: `https://api.hnyunzhu.com/api/v1/mobile/login?app_id=YOUR_APP_ID` > **提示**: 请将 `YOUR_APP_ID` 替换为您实际的应用 ID。 ### 第三步:实现回调接口 (后端) 用户登录成功后,浏览器会跳转到您配置的回调地址,URL 格式如下: `http://your-app.com/callback?ticket=TICKET-xxxxx` 您的后端需要接收 `ticket`,并调用 UAP 的验证接口换取用户信息。 **接口地址**: `POST https://api.hnyunzhu.com/api/v1/simple/validate` **请求参数 (JSON)**: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `app_id` | string | 是 | 您的应用 ID | | `ticket` | string | 是 | 接收到的票据 | | `timestamp` | int | 是 | 当前时间戳 | | `sign` | string | 是 | 签名 | ## 4. 代码示例 ### Python ```python import requests import time import hmac import hashlib import json 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 def validate_ticket(ticket): # API 地址通常为 /api/v1/simple/validate url = "https://api.hnyunzhu.com/api/v1/simple/validate" app_secret = "YOUR_APP_SECRET" # 务必保密 payload = { "app_id": "YOUR_APP_ID", "ticket": ticket, "timestamp": int(time.time()) } # 使用您的 App Secret 计算签名 payload["sign"] = generate_signature(app_secret, payload) try: resp = requests.post(url, json=payload) return resp.json() except Exception as e: print("Error:", e) return None ``` ### 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 TicketValidator { public static final String APP_ID = "YOUR_APP_ID"; public static final String APP_SECRET = "YOUR_APP_SECRET"; public static final String API_URL = "https://api.hnyunzhu.com/api/v1/simple/validate"; public static String generateSign(Map params, String secret) { try { List 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 validate(String ticket) { try { long timestamp = System.currentTimeMillis() / 1000; Map params = new HashMap<>(); params.put("app_id", APP_ID); params.put("ticket", ticket); params.put("timestamp", String.valueOf(timestamp)); String sign = generateSign(params, APP_SECRET); params.put("sign", sign); OkHttpClient client = new OkHttpClient(); RequestBody body = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), JSON.toJSONString(params) ); Request request = new Request.Builder() .url(API_URL) .post(body) .build(); Response response = client.newCall(request).execute(); System.out.println(response.body().string()); } catch (Exception e) { e.printStackTrace(); } } } ``` ### Kotlin ```kotlin import okhttp3.* import com.google.gson.Gson import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.io.IOException import java.util.* object AuthUtils { fun generateSign(params: Map, secret: String): String { val sortedKeys = params.keys.filter { it != "sign" }.sorted() val queryString = sortedKeys.joinToString("&") { "${it}=${params[it]}" } val hmacSha256 = "HmacSHA256" val secretKeySpec = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), hmacSha256) val mac = Mac.getInstance(hmacSha256) mac.init(secretKeySpec) return mac.doFinal(queryString.toByteArray(Charsets.UTF_8)) .joinToString("") { "%02x".format(it) } } } fun validateTicket(ticket: String) { val appId = "YOUR_APP_ID" val appSecret = "YOUR_APP_SECRET" val url = "https://api.hnyunzhu.com/api/v1/simple/validate" val timestamp = System.currentTimeMillis() / 1000 val params = mutableMapOf( "app_id" to appId, "ticket" to ticket, "timestamp" to timestamp.toString() ) params["sign"] = AuthUtils.generateSign(params, appSecret) val client = OkHttpClient() val jsonBody = Gson().toJson(params) val body = RequestBody.create(MediaType.parse("application/json"), jsonBody) val request = Request.Builder() .url(url) .post(body) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { e.printStackTrace() } override fun onResponse(call: Call, response: Response) { println(response.body()?.string()) } }) } ``` ### Node.js ```javascript const axios = require('axios'); const crypto = require('crypto'); const APP_SECRET = 'YOUR_APP_SECRET'; // 务必保密 function getSign(params) { const keys = Object.keys(params) .filter(k => k !== 'sign' && params[k] !== undefined) .sort(); const queryString = keys.map(k => `${k}=${params[k]}`).join('&'); return crypto.createHmac('sha256', APP_SECRET) .update(queryString) .digest('hex'); } async function validateTicket(ticket) { const url = 'https://api.hnyunzhu.com/api/v1/simple/validate'; const payload = { app_id: 'YOUR_APP_ID', ticket: ticket, timestamp: Math.floor(Date.now() / 1000) }; // 计算签名 payload.sign = getSign(payload); try { const res = await axios.post(url, payload); console.log('Validation Result:', res.data); return res.data; } catch (error) { console.error('Validation Failed:', error.response?.data || error.message); } } ``` ### Go ```go package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "sort" "strings" "time" ) func GetSign(secret string, params map[string]interface{}) string { var keys []string for k := range params { if k != "sign" { keys = append(keys, k) } } sort.Strings(keys) 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, "&") h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(query)) return hex.EncodeToString(h.Sum(nil)) } func ValidateTicket(ticket string) { appId := "YOUR_APP_ID" appSecret := "YOUR_APP_SECRET" apiUrl := "https://api.hnyunzhu.com/api/v1/simple/validate" params := map[string]interface{}{ "app_id": appId, "ticket": ticket, "timestamp": time.Now().Unix(), } // 计算签名 params["sign"] = GetSign(appSecret, params) jsonData, _ := json.Marshal(params) resp, err := http.Post(apiUrl, "application/json", bytes.NewBuffer(jsonData)) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() // 读取响应... var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Result:", result) } ``` ### Swift ```swift import Foundation import CommonCrypto // 注意:需引入 CommonCrypto 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 { let sortedKeys = params.keys.filter { $0 != "sign" }.sorted() let queryParts = sortedKeys.map { key in return "\(key)=\(params[key]!)" } let queryString = queryParts.joined(separator: "&") return hmac(string: queryString, key: secret) } func validateTicket(ticket: String) { let appId = "YOUR_APP_ID" let secret = "YOUR_APP_SECRET" let url = URL(string: "https://api.hnyunzhu.com/api/v1/simple/validate")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let timestamp = Int(Date().timeIntervalSince1970) var params: [String: Any] = [ "app_id": appId, "ticket": ticket, "timestamp": timestamp ] params["sign"] = generateSign(secret: secret, params: params) request.httpBody = try? JSONSerialization.data(withJSONObject: params) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { if let json = try? JSONSerialization.jsonObject(with: data, options: []) { print(json) } } } task.resume() } ``` ## 5. 响应格式 (成功) ```json { "valid": true, "user_id": 1001, "mobile": "13800138000", "mapped_key": "user_zhangsan", "mapped_email": "zhangsan@example.com" } ```