feat: AuthSDK 认证中心完整实现(对齐 C# AuthSDK)

- SimApiAuthClient:SimApiHttpClient 子类,凭证取 AuthCenterOptions
- SimApiAuthCenter:群组/Profile/内部应用/系统登录/安全验证 12 接口 + VerifySign
- SimApiAuthIam:注册权限/获取权限/校验权限(无权限 403)
- SimApiAuthCenterMiddleware:网关透传(三头 MD5 校验 + Base64 解码 LoginInfo)
- SimApiAuthDto:7 个 DTO(data 字段用 JsonValue 规避宏约束)
This commit is contained in:
2026-08-16 22:46:48 +08:00
parent 9af7871114
commit f056de582b
5 changed files with 496 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* AuthSDK/SimApiAuthCenter:认证中心远程 SDK。
*/
package simapi.authsdk
import std.collection.*
import stdx.net.tls.*
import stdx.net.tls.common.*
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
import simapi.communications.*
import simapi.helpers.*
/**
* 认证中心远程 SDK(对齐 C# SimApiAuthCenter):
* 群组 / Profile / 内部应用 / 系统登录 / 安全验证 等接口,走签名请求。
*/
public class SimApiAuthCenter {
private let _client: SimApiAuthClient
public init(client: SimApiAuthClient) {
this._client = client
}
public prop client: SimApiAuthClient {
get() {
_client
}
}
// ===== 公共 =====
/**
* 委托 AuthCenter 进行应用签名验证(对齐 C# VerifySign)。
*/
public func verifySign(appId: String, timestamp: String, nonce: String, sign: String): Unit {
let url = "${_client.server}/api/auth/sign/verify?appId=${appId}&timestamp=${timestamp}&nonce=${nonce}&sign=${sign}"
let http = HttpClient.create { builder =>
builder.noProxy()
var tls = TlsClientConfig()
tls.verifyMode = CertificateVerifyMode.TrustAll
match (_client.server.indexOf("://")) {
case Some(i) =>
let rest = _client.server[i + 3..]
let slash = rest.indexOf("/") ?? rest.size
let q = rest.indexOf("?") ?? rest.size
let end = if (slash < q) { slash } else { q }
let host = rest[0..end]
if (!host.isEmpty()) {
tls.serverName = Some(host)
}
case None => ()
}
builder.tlsConfig(tls)
}
try {
let request = HttpRequestMessage(NetHttpMethod.Post, url)
request.content = JsonContent.create("{}")
let response = http.send(request)
try {
response.ensureSuccessStatusCode()
let resp = response.content.readFromJson<SimApiBaseResponse>()
SimApiError.errorWhen(resp._code != 200, code: 400, message: "签名验证失败")
} finally {
response.close()
}
} finally {
http.close()
}
}
// ===== 群组相关 =====
/**
* 根据 profileId 获取群组列表(对齐 C# GroupRelated)。
*/
public func groupRelated(profileId: String): Array<GroupRelatedItem> {
_client.signQuery<Array<GroupRelatedItem>>("/api/auth/group/related",
body: simpleBody("profileId", profileId))
}
/**
* 按关键字搜索群组,输入群组 ID 精准搜索(对齐 C# GroupSearch)。
*/
public func groupSearch(keyword: String, skip!: Int64 = 0, take!: Int64 = 20): Array<AppAndProfileItem> {
var body = HashMap<String, Any>()
body["keyword"] = keyword
body["skip"] = skip
body["take"] = take
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/group/search", body: SimApiJson.json(Some(body)))
}
/**
* 使用组 ID 以及组内成员/管理员 profile 获取组的详细树结构(对齐 C# GroupDetail)。
*/
public func groupDetail(groupId: String, profileId: String): GroupDetailTreeNode {
var body = HashMap<String, Any>()
body["profileId"] = profileId
body["groupId"] = groupId
_client.signQuery<GroupDetailTreeNode>("/api/auth/group/detail", body: SimApiJson.json(Some(body)))
}
/**
* 获取 profile 在本组的所有子组(对齐 C# GroupRelatedIndex)。
*/
public func groupRelatedIndex(groupId: String, profileId: String): Array<String> {
var body = HashMap<String, Any>()
body["groupId"] = groupId
body["profileId"] = profileId
_client.signQuery<Array<String>>("/api/auth/internal/group/related-group-ids",
body: SimApiJson.json(Some(body)))
}
// ===== Profile 相关 =====
/**
* 按关键字搜索用户 Profile(对齐 C# ProfileSearch)。
*/
public func profileSearch(keyword: String, skip!: Int64 = 0, take!: Int64 = 20): Array<AppAndProfileItem> {
var body = HashMap<String, Any>()
body["keyword"] = keyword
body["skip"] = skip
body["take"] = take
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/search", body: SimApiJson.json(Some(body)))
}
/**
* 通过 id 批量获取用户基本信息(对齐 C# ProfileList)。
*/
public func profileList(ids: Array<String>): Array<AppAndProfileItem> {
var body = HashMap<String, Any>()
var arr = ArrayList<Any>()
for (id in ids) {
arr.add(id)
}
body["ids"] = arr.toArray()
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/list", body: SimApiJson.json(Some(body)))
}
// ===== AuthGate 内部应用专用 =====
/**
* 获取是否为 App 的拥有者(对齐 C# CheckIsAppOwner,字段为 PascalCase)。
*/
public func checkIsAppOwner(profileId: String, applicationId: String): Bool {
var body = HashMap<String, Any>()
body["ProfileId"] = profileId
body["AppId"] = applicationId
_client.signQuery<Bool>("/api/auth/internal/app/check-owner", body: SimApiJson.json(Some(body)))
}
/**
* 根据用户 profileId 和提供的 appIds 获取应用列表(对齐 C# GetAppList,字段为 PascalCase)。
*/
public func getAppList(profileId: String, appIds: Array<String>): Array<AppAndProfileItem> {
var body = HashMap<String, Any>()
body["ProfileId"] = profileId
var arr = ArrayList<Any>()
for (id in appIds) {
arr.add(id)
}
body["AllowedAppIds"] = arr.toArray()
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/internal/app/related",
body: SimApiJson.json(Some(body)))
}
// ===== 系统登录 =====
/**
* 获取登录授权 CODE(对齐 C# GetLoginCode)。
* @param scene 场景标识。
* @param data 附加数据。
* @param backUrl 回调地址。
* @return GetCodeResponse(含 Code/Server/FullUrl)。
*/
public func getLoginCode(scene!: ?String = None, data!: ?HashMap<String, Any> = None,
backUrl!: ?String = None): GetCodeResponse {
var body = HashMap<String, Any>()
if (let Some(scene) <- scene) { body["scene"] = scene }
if (let Some(data) <- data) { body["data"] = data }
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
let code = _client.signQuery<String>("/api/auth/login/code", body: SimApiJson.json(Some(body)))
let server = _client.server
GetCodeResponse(code, server, "${server}/auth?code=${code}")
}
/**
* 使用 code 获取登录信息(对齐 C# GetLoginInfo,场景不匹配抛 403003)。
*/
public func getLoginInfo(code: String, scene!: ?String = None): LoginInfoResponse {
var body = HashMap<String, Any>()
body["code"] = code
let resp = _client.signQuery<LoginInfoResponse>("/api/auth/login/get", body: SimApiJson.json(Some(body)))
SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "登录场景不匹配")
resp
}
// ===== 安全验证 =====
/**
* 获取安全验证代码(对齐 C# GetConfirmCode)。
*/
public func getConfirmCode(scene: String, userId: String, data!: ?HashMap<String, Any> = None,
backUrl!: ?String = None): GetCodeResponse {
var body = HashMap<String, Any>()
body["scene"] = scene
if (let Some(data) <- data) { body["data"] = data }
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
body["profileId"] = userId
let code = _client.signQuery<String>("/api/auth/confirm/code", body: SimApiJson.json(Some(body)))
let server = _client.server
GetCodeResponse(code, server, "${server}/confirm?code=${code}")
}
/**
* 使用安全验证 code 获取验证结果(对齐 C# Confirm,身份/场景不匹配分别抛 403002/403003)。
*/
public func confirm(code: String, scene: String, userId!: ?String = None): ConfirmResponse {
var body = HashMap<String, Any>()
body["code"] = code
let resp = _client.signQuery<ConfirmResponse>("/api/auth/confirm/get", body: SimApiJson.json(Some(body)))
SimApiError.errorWhen(userId != Some(resp._profileId), code: 403002, message: "安全确认身份不匹配")
SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "安全确认场景不匹配")
resp
}
/// 简单单字段请求体:{"field":"value"}
private static func simpleBody(field: String, value: String): String {
"{\"${field}\":\"${SimApiJson.escapeJson(value)}\"}"
}
}