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:
@@ -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<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)}\"}"
|
||||
}
|
||||
}
|
||||
@@ -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<SimApiLoginItem>(authValue)
|
||||
context.items["LoginInfo"] = login
|
||||
}
|
||||
}
|
||||
}
|
||||
next(context)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<GroupDetailTreeNode> = []
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<PermissionItem>): Unit {
|
||||
// 请求体:{"permissions":[{"identifier":...,"name":...,"group":...,"description":...},...]}
|
||||
var items = ArrayList<Any>()
|
||||
for (p in permissions) {
|
||||
var item = HashMap<String, Any>()
|
||||
item["identifier"] = p._identifier
|
||||
item["name"] = p._name
|
||||
item["group"] = p._group
|
||||
item["description"] = p._description
|
||||
items.add(item)
|
||||
}
|
||||
var body = HashMap<String, Any>()
|
||||
body["permissions"] = items.toArray()
|
||||
_client.signQuery<String>("/api/iam/permission/register", body: SimApiJson.json(Some(body)))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拥有的权限标识数组(对齐 C# GetPermissionOwned)。
|
||||
*/
|
||||
public func getPermissionOwned(profileId: String, groupId!: ?String = None): Array<String> {
|
||||
var body = HashMap<String, Any>()
|
||||
body["profileId"] = profileId
|
||||
if (let Some(groupId) <- groupId) {
|
||||
body["groupId"] = groupId
|
||||
}
|
||||
_client.signQuery<Array<String>>("/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<String, Any>()
|
||||
body["profileId"] = profileId
|
||||
body["permission"] = permission
|
||||
if (let Some(groupId) <- groupId) {
|
||||
body["groupId"] = groupId
|
||||
}
|
||||
let ok = _client.signQuery<Bool>("/api/iam/permission/check", body: SimApiJson.json(Some(body)))
|
||||
SimApiError.errorWhen(!ok, code: 403, message: "没有该权限")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user