From f056de582bae3c1c6eb4e74c19df719bf63fc34d Mon Sep 17 00:00:00 2001 From: xRain Date: Sun, 16 Aug 2026 22:46:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AuthSDK=20=E8=AE=A4=E8=AF=81=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0=EF=BC=88=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20C#=20AuthSDK=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SimApiAuthClient:SimApiHttpClient 子类,凭证取 AuthCenterOptions - SimApiAuthCenter:群组/Profile/内部应用/系统登录/安全验证 12 接口 + VerifySign - SimApiAuthIam:注册权限/获取权限/校验权限(无权限 403) - SimApiAuthCenterMiddleware:网关透传(三头 MD5 校验 + Base64 解码 LoginInfo) - SimApiAuthDto:7 个 DTO(data 字段用 JsonValue 规避宏约束) --- src/authsdk/SimApiAuthCenter.cj | 234 ++++++++++++++++++++++ src/authsdk/SimApiAuthCenterMiddleware.cj | 45 +++++ src/authsdk/SimApiAuthClient.cj | 27 +++ src/authsdk/SimApiAuthDto.cj | 122 +++++++++++ src/authsdk/SimApiAuthIam.cj | 68 +++++++ 5 files changed, 496 insertions(+) create mode 100644 src/authsdk/SimApiAuthCenter.cj create mode 100644 src/authsdk/SimApiAuthCenterMiddleware.cj create mode 100644 src/authsdk/SimApiAuthClient.cj create mode 100644 src/authsdk/SimApiAuthDto.cj create mode 100644 src/authsdk/SimApiAuthIam.cj diff --git a/src/authsdk/SimApiAuthCenter.cj b/src/authsdk/SimApiAuthCenter.cj new file mode 100644 index 0000000..f7dbafe --- /dev/null +++ b/src/authsdk/SimApiAuthCenter.cj @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\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}×tamp=${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() + SimApiError.errorWhen(resp._code != 200, code: 400, message: "签名验证失败") + } finally { + response.close() + } + } finally { + http.close() + } + } + + // ===== 群组相关 ===== + + /** + * 根据 profileId 获取群组列表(对齐 C# GroupRelated)。 + */ + public func groupRelated(profileId: String): Array { + _client.signQuery>("/api/auth/group/related", + body: simpleBody("profileId", profileId)) + } + + /** + * 按关键字搜索群组,输入群组 ID 精准搜索(对齐 C# GroupSearch)。 + */ + public func groupSearch(keyword: String, skip!: Int64 = 0, take!: Int64 = 20): Array { + var body = HashMap() + body["keyword"] = keyword + body["skip"] = skip + body["take"] = take + _client.signQuery>("/api/auth/group/search", body: SimApiJson.json(Some(body))) + } + + /** + * 使用组 ID 以及组内成员/管理员 profile 获取组的详细树结构(对齐 C# GroupDetail)。 + */ + public func groupDetail(groupId: String, profileId: String): GroupDetailTreeNode { + var body = HashMap() + body["profileId"] = profileId + body["groupId"] = groupId + _client.signQuery("/api/auth/group/detail", body: SimApiJson.json(Some(body))) + } + + /** + * 获取 profile 在本组的所有子组(对齐 C# GroupRelatedIndex)。 + */ + public func groupRelatedIndex(groupId: String, profileId: String): Array { + var body = HashMap() + body["groupId"] = groupId + body["profileId"] = profileId + _client.signQuery>("/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 { + var body = HashMap() + body["keyword"] = keyword + body["skip"] = skip + body["take"] = take + _client.signQuery>("/api/auth/profile/search", body: SimApiJson.json(Some(body))) + } + + /** + * 通过 id 批量获取用户基本信息(对齐 C# ProfileList)。 + */ + public func profileList(ids: Array): Array { + var body = HashMap() + var arr = ArrayList() + for (id in ids) { + arr.add(id) + } + body["ids"] = arr.toArray() + _client.signQuery>("/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() + body["ProfileId"] = profileId + body["AppId"] = applicationId + _client.signQuery("/api/auth/internal/app/check-owner", body: SimApiJson.json(Some(body))) + } + + /** + * 根据用户 profileId 和提供的 appIds 获取应用列表(对齐 C# GetAppList,字段为 PascalCase)。 + */ + public func getAppList(profileId: String, appIds: Array): Array { + var body = HashMap() + body["ProfileId"] = profileId + var arr = ArrayList() + for (id in appIds) { + arr.add(id) + } + body["AllowedAppIds"] = arr.toArray() + _client.signQuery>("/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 = None, + backUrl!: ?String = None): GetCodeResponse { + var body = HashMap() + 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("/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() + body["code"] = code + let resp = _client.signQuery("/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 = None, + backUrl!: ?String = None): GetCodeResponse { + var body = HashMap() + 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("/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() + body["code"] = code + let resp = _client.signQuery("/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)}\"}" + } +} diff --git a/src/authsdk/SimApiAuthCenterMiddleware.cj b/src/authsdk/SimApiAuthCenterMiddleware.cj new file mode 100644 index 0000000..ad943d5 --- /dev/null +++ b/src/authsdk/SimApiAuthCenterMiddleware.cj @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * AuthSDK/SimApiAuthCenterMiddleware:网关透传认证中间件。 + */ + +package simapi.authsdk + +import soulsoft_web_http.* +import simapi.communications.* +import simapi.configurations.* +import simapi.helpers.* + +/** + * 网关透传认证中间件(对齐 C# SimApiAuthCenterMiddleware): + * 当请求带 X-SimApi-Gate-Auth / X-SimApi-Gate-Time / X-SimApi-Gate-Sign 三头时, + * 校验 MD5 签名(appId=..&auth=..&time=..&appKey=..),通过则 Base64 解码登录信息写入 LoginInfo。 + */ +public class SimApiAuthCenterMiddleware <: IMiddleware { + private let _options: SimApiOptions + + public init(options: SimApiOptions) { + this._options = options + } + + public func invoke(context: HttpContext, next: RequestDelegate): Unit { + let auth = context.request.headers.get("X-SimApi-Gate-Auth") + let time = context.request.headers.get("X-SimApi-Gate-Time") + let sign = context.request.headers.get("X-SimApi-Gate-Sign") + if (auth != None && time != None && sign != None) { + let authValue = auth.getOrThrow() + let timeValue = time.getOrThrow() + let signValue = sign.getOrThrow() + if (!authValue.isEmpty()) { + let authOptions = _options.simApiAuthCenterOptions + let signStr = "appId=${authOptions.appId}&auth=${authValue}&time=${timeValue}&appKey=${authOptions.appKey}" + if (SimApiUtil.md5(signStr) == signValue) { + let login = SimApiUtil.base64DecodeTo(authValue) + context.items["LoginInfo"] = login + } + } + } + next(context) + } +} diff --git a/src/authsdk/SimApiAuthClient.cj b/src/authsdk/SimApiAuthClient.cj new file mode 100644 index 0000000..dc92424 --- /dev/null +++ b/src/authsdk/SimApiAuthClient.cj @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * AuthSDK/SimApiAuthClient:认证中心专用签名客户端。 + */ + +package simapi.authsdk + +import simapi.configurations.* +import simapi.helpers.* + +/** + * 认证中心签名客户端(对齐 C# SimApiAuthClient): + * SimApiHttpClient 子类,凭证(Server/AppId/AppKey)取自 SimApiAuthCenterOptions。 + */ +public class SimApiAuthClient <: SimApiHttpClient { + /** + * @param options SimApi 配置(使用 simApiAuthCenterOptions 的 Server/AppId/AppKey)。 + */ + public init(options: SimApiOptions) { + super(options: options) + let auth = options.simApiAuthCenterOptions + server = auth.server + appId = auth.appId + appKey = auth.appKey + } +} diff --git a/src/authsdk/SimApiAuthDto.cj b/src/authsdk/SimApiAuthDto.cj new file mode 100644 index 0000000..6dfb52c --- /dev/null +++ b/src/authsdk/SimApiAuthDto.cj @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * AuthSDK 用到的 DTO(对齐 C# SimApiAuthCenterDto / SimApiAuthIamDto)。 + */ + +package simapi.authsdk + +import std.collection.* +import stdx.encoding.json.* +import soulsoft_serialization.* +import soulsoft_serialization.macros.* + +/** + * 应用/Profile 通用项(对齐 C# AppAndProfileItem)。 + */ +@Serialization +public class AppAndProfileItem { + public var _id: String = "" + public var _name: String = "" + public var _image: ?String = None + public var _description: ?String = None + + public init() {} +} + +/** + * 安全确认响应(对齐 C# ConfirmResponse)。 + */ +@Serialization +public class ConfirmResponse { + public var _applicationId: String = "" + public var _profileId: String = "" + public var _scene: ?String = None + public var _data: ?JsonValue = None + + public init() {} +} + +/** + * 登录信息响应(对齐 C# LoginInfoResponse)。 + */ +@Serialization +public class LoginInfoResponse { + public var _scene: ?String = None + public var _data: ?JsonValue = None + public var _profileId: String = "" + public var _name: String = "" + public var _image: ?String = None + public var _description: ?String = None + + public init() {} +} + +/** + * 获取授权码响应(对齐 C# GetCodeResponse)。 + */ +@Serialization +public class GetCodeResponse { + public var _code: String = "" + public var _server: String = "" + public var _fullUrl: String = "" + + public init() {} + + public init(code: String, server: String, fullUrl: String) { + this._code = code + this._server = server + this._fullUrl = fullUrl + } +} + +/** + * 群组关联项(对齐 C# GroupRelatedItem)。 + */ +@Serialization +public class GroupRelatedItem { + public var _id: String = "" + public var _name: String = "" + public var _image: ?String = None + public var _description: ?String = None + public var _isOwner: Bool = false + public var _isAdmin: Bool = false + public var _isMember: Bool = false + + public init() {} +} + +/** + * 群组详情树节点(对齐 C# GroupDetailTreeNode,children 递归)。 + */ +@Serialization +public class GroupDetailTreeNode { + public var _id: String = "" + public var _name: String = "" + public var _image: ?String = None + public var _description: ?String = None + public var _sort: Int64 = 0 + public var _children: Array = [] + + public init() {} +} + +/** + * 权限项(对齐 C# PermissionItem)。 + */ +@Serialization +public class PermissionItem { + public var _identifier: String = "" + public var _name: String = "" + public var _group: String = "" + public var _description: String = "" + + public init() {} + + public init(identifier: String, name: String, group: String, description: String) { + this._identifier = identifier + this._name = name + this._group = group + this._description = description + } +} diff --git a/src/authsdk/SimApiAuthIam.cj b/src/authsdk/SimApiAuthIam.cj new file mode 100644 index 0000000..6e13acb --- /dev/null +++ b/src/authsdk/SimApiAuthIam.cj @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * AuthSDK/SimApiAuthIam:权限中心远程 SDK。 + */ + +package simapi.authsdk + +import std.collection.* +import simapi.communications.* +import simapi.helpers.* + +/** + * 权限中心远程 SDK(对齐 C# SimApiAuthIam): + * 注册权限点 / 获取权限标识 / 校验权限。 + */ +public class SimApiAuthIam { + private let _client: SimApiAuthClient + + public init(client: SimApiAuthClient) { + this._client = client + } + + /** + * 向 IAM 注册权限(对齐 C# RegisterPermissions)。 + */ + public func registerPermissions(permissions: Array): Unit { + // 请求体:{"permissions":[{"identifier":...,"name":...,"group":...,"description":...},...]} + var items = ArrayList() + for (p in permissions) { + var item = HashMap() + item["identifier"] = p._identifier + item["name"] = p._name + item["group"] = p._group + item["description"] = p._description + items.add(item) + } + var body = HashMap() + body["permissions"] = items.toArray() + _client.signQuery("/api/iam/permission/register", body: SimApiJson.json(Some(body))) + } + + /** + * 获取拥有的权限标识数组(对齐 C# GetPermissionOwned)。 + */ + public func getPermissionOwned(profileId: String, groupId!: ?String = None): Array { + var body = HashMap() + body["profileId"] = profileId + if (let Some(groupId) <- groupId) { + body["groupId"] = groupId + } + _client.signQuery>("/api/iam/permission/owned", body: SimApiJson.json(Some(body))) + } + + /** + * 检测 profileId 是否有该权限,无权限抛 403(对齐 C# CheckPermission)。 + */ + public func checkPermission(profileId: String, permission: String, groupId!: ?String = None): Unit { + var body = HashMap() + body["profileId"] = profileId + body["permission"] = permission + if (let Some(groupId) <- groupId) { + body["groupId"] = groupId + } + let ok = _client.signQuery("/api/iam/permission/check", body: SimApiJson.json(Some(body))) + SimApiError.errorWhen(!ok, code: 403, message: "没有该权限") + } +}