|
|
@@ -0,0 +1,392 @@
|
|
|
+# 快速对接指南 (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<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 validate(String ticket) {
|
|
|
+ try {
|
|
|
+ long timestamp = System.currentTimeMillis() / 1000;
|
|
|
+
|
|
|
+ Map<String, String> 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<String, String>, 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"
|
|
|
+}
|
|
|
+```
|