Files
simapi-cj/src/authsdk/SimApiAuthCenter.cj
T
xrain 92eea3108a refactor: 对齐 simapi-net 的 20 项差异
- SimApiLoginItem._types 默认 [user]
- 新增 SimApiIdOnlyRequest
- errorWhenNone 改名 errorWhenNull
- SimApiAuth.update 保留 TTL(Redis ttl/expire)
- SimApiCache: set 接受 Any + 新增 get<T>/getString
- SimApiHttpClient 签名字段移出 Options
- SimApiUtil: md5/sha1 支持 mode + 新增 paginate
- @SimApiAuth 支持逗号分隔多类型
- Logger: 4 位毫秒 + IsEnabled 恒 true
- 删除 /versions 端点;exceptionHandler 改为抛错
- UseSimApi 中间件顺序对齐 .NET
- README 同步更新
2026-08-16 23:44:52 +08:00

237 lines
9.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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
// 说明:C# 的 ErrorWhenNull(resp, 400232, "登录信息获取失败") 对应 signQuery 内部 _data.getOrThrow() 的
// None 分支;仓颉版 signQuery 返回非空 T(data 缺失即抛异常),故此处无需重复判空。
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)}\"}"
}
}