From 40e07260872f02caaedc537febc41c9d85b0f882 Mon Sep 17 00:00:00 2001 From: xRain Date: Sun, 16 Aug 2026 22:47:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E6=9B=B4=E6=96=B0=EF=BC=88?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=E6=96=87=E6=A1=A3=20+=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=BF=87=E6=97=B6=E7=8A=B6=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增:@SimApiAuth/@OriginResponse/验签/AES body/AES/BaseModel/AuthSDK 使用文档 - 修正:AuthGate/AES 已实现、自动控制器扫描、DI 构造示例、移除 ApiResult 引用 - cjpm.lock:新增 soulsoft_net_http 等依赖 --- README.md | 233 ++++++++++++++++++++++++++++++++++++++++++++++-------- cjpm.lock | 24 +++--- 2 files changed, 213 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 27c16c3..6d0c45d 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,20 @@ main(args: Array) { simapi-cj/ ├── cjpm.toml # 包配置 ├── src/ -│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, ApiResult, 请求 DTO +│ ├── attributes/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应) +│ ├── authsdk/ # 认证中心 SDK:SimApiAuthClient/Center/Iam + 网关中间件 + DTO +│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO │ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调) │ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController(MVC 写法) │ ├── exceptions/ # SimApiException -│ ├── extensions/ # SimApiExtensions(addSimApi / useSimApi + 内置路由) -│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient +│ ├── extensions/ # SimApiExtensions(addSimApi / useSimApi + 内置路由 + 响应封装) +│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient, +│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body) │ ├── interfaces/ # ISimApiAuthChecker │ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志) -│ └── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware +│ ├── macros/ # ReadTomlVersion(编译期读版本号) +│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware +│ └── models/ # SimApiBaseModel(实体基类) ``` --- @@ -106,7 +111,8 @@ SimApiError.errorWhenNone(someOptional, 404, "用户不存在") import simapi.helpers.* import simapi.communications.* -let auth = SimApiAuth(redisConfiguration: "") // 配 Redis 用 Redis,否则 InMemory +// 由 DI 注入(构造参数 options: SimApiOptions,从配置读 RedisConfiguration;未配则 InMemory) +let auth: SimApiAuth = ... // 例:控制器构造注入 let token = auth.login(SimApiLoginItem(id: "user-001")) // 默认 7 天 let login = auth.getLogin(token) // 获取登录信息 @@ -114,14 +120,94 @@ auth.logout(token) // 退出登录 auth.logoutAll("user-001") // 退出全部 ``` -- **Redis 模式**:配置 `RedisConfiguration`(如 `"localhost:6379"`)时使用,支持多实例共享 -- **InMemory 模式**:零配置,适合开发/测试;重启后登录态丢失 +- **Redis 模式**:配置 `RedisConfiguration` 时使用,支持多实例共享。连接串格式: + - `"localhost:6379"`(基础) + - `"localhost:6379,password=xxx"`(带密码) + - `"localhost:6379,password=xxx,db=2"`(带密码 + DB 索引) +- **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失 - **Token 传参**:Header `Token: ` 或 Query `token=` +### 2.1 声明式鉴权 — @SimApiAuth(注解类,对齐 C# [SimApiAuth]) + +标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`),替代手动 `requireLogin()`: + +```cangjie +import simapi.attributes.{SimApiAuth} + +@SimApiAuth // 类级:整个控制器需登录 +public class MyController <: SimApiBaseController { + + @SimApiAuth["admin"] // 方法级:仅 admin 类型可访问 + @HttpPost["my/admin-only"] + public func adminOnly(): String { "ok" } +} +``` + +> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单类型参数(`@SimApiAuth["admin"]`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。 + +### 2.2 原样响应 — @OriginResponse + +标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`): + +```cangjie +import simapi.attributes.{OriginResponse} + +@OriginResponse +@HttpGet["raw"] +public func raw(): String { + "{\"raw\":true}" // 直接输出,不包 {code,message,data} +} +``` + +### 2.3 服务端验签 — SimApiSignChecker(对齐 C# [SimApiSign]) + +校验带签名请求(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对): + +```cangjie +import simapi.helpers.{SimApiSignProviderBase, SimApiSignChecker} + +// 1. 继承 Provider 实现密钥获取 +public class MySignProvider <: SimApiSignProviderBase { + public override func getKey(appId: ?String): ?String { + // 根据 appId 返回密钥(如查库) + Some("my-secret-key") + } +} + +// 2. 控制器方法开头调用校验 +public func signedAction(): String { + SimApiSignChecker.verify(context, provider, cache) + "ok" +} +``` + +Provider 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# `SimApiSignProviderBase` 一致)。 + +### 2.4 AES body 解密 — SimApiAesBodyChecker(对齐 C# [AesBody]) + +服务端接收 `{"data":"密文"}` 加密 body,解密后返回明文 JSON(控制器再反序列化为目标类型): + +```cangjie +import simapi.helpers.{AesBodyProviderBase, SimApiAesBodyChecker} + +public class MyAesProvider <: AesBodyProviderBase { + public override func getKey(appId: ?String): ?String { + Some("aes-secret-key") + } +} + +public func create(@FromBody req: AesBodyRequest): String { + let json = SimApiAesBodyChecker.decryptBody(context, provider) // 解密后的 JSON 字符串 + let dto = JsonSerializer.deserializeObject(json) + "ok" +} +``` + ### 3. 缓存 — SimApiCache ```cangjie -let cache = SimApiCache(redisConfiguration: "") +// 由 DI 注入(构造参数 options: SimApiOptions) +let cache: SimApiCache = ... cache.set("key", "value") let v = cache.get("key") // ?String cache.hasKey("key") // Bool @@ -135,16 +221,47 @@ Key 自动加前缀 `SimApi:Cache:`。 ```cangjie SimApiUtil.cstNow // UTC+8 时间 SimApiUtil.timestampNow // 秒级时间戳 +SimApiUtil.newGuid() // UUID v4(对齐 C# Guid.NewGuid()) SimApiUtil.md5("text") // 32 位十六进制 SimApiUtil.sha1("text") // 40 位 SimApiUtil.base64Encode("text") / base64Decode("...") +SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object)) +SimApiUtil.fromJson(json) // JSON → T(对齐 C# FromJson,T 需 ISerialization) +SimApiUtil.base64DecodeTo(str) // Base64 → JSON → T(对齐 C# Base64Decode) SimApiUtil.checkCell("13800138000") // 手机号 SimApiUtil.checkEmail("a@b.com") // 邮箱 ``` +### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil) + +纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证: + +```cangjie +let encrypted = SimApiAesUtil.encrypt("明文", "key字符串") // Base64(随机IV + 密文) +let plain = SimApiAesUtil.decrypt(encrypted, "key字符串") +``` + +- 密钥:`SHA256(key 字符串)` → 32 字节;IV 每次随机 16 字节前置;输出 `Base64(IV + 密文)` +- 供 `SimApiHttpClient.aesQuery` / `aesSignQuery` 使用 + +### 4.2 实体基类 — SimApiBaseModel(对齐 C# SimApiBaseModel) + +```cangjie +import simapi.models.* + +public class User <: SimApiBaseModel { + public var _name: String = "" +} + +let user = User() // _id 自动 GUID、_createdAt/_updatedAt 自动当前时间 +user.mapData(source) // 反射映射:源对象同名同类型字段 → this(忽略 Id/CreatedAt/UpdatedAt) +user.mapData(source, ["_name"]) // 白名单映射 +user.updateTime() // 刷新 _updatedAt +``` + ### 5. HTTP 客户端 — SimApiHttpClient -用于调用其他带签名/AES 的 SimApi 服务: +用于调用其他带签名/AES 的 SimApi 服务(**内置 TLS 支持**:`https` 自动配置信任所有证书 + SNI,仓颉生态下 stdx TLS 动态加载 openssl 可用): ```cangjie let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey @@ -155,15 +272,20 @@ let resp2 = client.aesQuery("/api/data", body: "{\"a\":1}") let resp3 = client.aesSignQuery("/api/data", body: "{\"a\":1}") ``` +签名参数名可配置(`simApiHttpClientOptions.signName / timestampName / nonceName / appIdName / signFields`,C# 侧为硬编码)。 + ### 5.1 请求日志 — enableRequestLog -记录每次请求的方法、URL、请求头、请求体、响应状态码与耗时: +记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#): +- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断) +- 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo) +- 响应体因 soulsoft `HttpResponse.body` 只读不可替换,记录 `Content-Length` 作为替代(C# 用 MemoryStream 捕获) ```cangjie builder.addSimApi { options => options.enableRequestLog = true options.simApiRequestLogOptions.showFullHeader = true // 打印完整 Header(默认只打 Token/Query-Id) - options.simApiRequestLogOptions.requestStringLogLength = 200 // 请求体截断长度(0 不截断) + options.simApiRequestLogOptions.requestStringLogLength = 200 // 请求体字段截断长度(0 不截断) } ``` @@ -174,7 +296,7 @@ builder.addSimApi { options => *( RequestHeaders [Full] ) => {"host":"127.0.0.1:5000",...} *( RequestBody ) => - +{"name":"AAAA...(200)","image":"x"} *( Response [200] ) => 1.756400ms ``` @@ -207,8 +329,20 @@ builder.addSimApi { options => | `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 | | `/exception/{code}` | GET | 始终 | 错误反馈 | +路由路径可自定义(`configureSimApiRoute`,自定义值通过 `mapGet/mapPost` 真实注册,默认值由内置控制器特性路由覆盖): + +```cangjie +options.configureSimApiRoute { route => + route.userInfoRoute = Some("/my/user/info") // 自定义路径生效 + route.logoutRoute = Some("/my/auth/logout") + route.webConfigRoute = Some("/my/config") +} +``` + ### 7. 认证后处理 Hook — ISimApiAuthChecker +实现后每次认证成功都会调用(配合 `@SimApiAuth` 注解或手动 `requireLogin`): + ```cangjie import simapi.interfaces.* @@ -219,13 +353,45 @@ class MyAuthChecker <: ISimApiAuthChecker { } ``` +### 8. 认证中心 SDK — AuthSDK(对齐 C# AuthSDK) + +`enableSimApiAuthGate = true` 时注册 `SimApiAuthClient` / `SimApiAuthCenter` / `SimApiAuthIam` 单例并挂载网关透传中间件: + +```cangjie +builder.addSimApi { options => + options.enableSimApiAuthGate = true + options.configureSimApiAuthCenter { auth => + auth.server = "https://auth.example.com" + auth.appId = "app-id" + auth.appKey = "app-key" + } +} +``` + +| 类 | 说明 | +|----|------| +| `SimApiAuthClient` | `SimApiHttpClient` 子类,凭证取 AuthCenterOptions | +| `SimApiAuthCenter` | 群组/Profile/内部应用/系统登录/安全验证等 12 个接口 + `VerifySign` | +| `SimApiAuthIam` | 注册权限 / 获取权限标识 / 校验权限(无权限抛 403) | +| `SimApiAuthCenterMiddleware` | 网关透传:`X-SimApi-Gate-Auth/Time/Sign` 三头 MD5 校验 → Base64 解码 LoginInfo | + +```cangjie +import simapi.authsdk.* + +let center = SimApiAuthCenter(client) // client 从 DI 注入 +let groups = center.groupRelated(profileId) // 群组列表 +let loginInfo = center.getLoginInfo(code) // 登录信息(场景校验) +let iam = SimApiAuthIam(client) +iam.checkPermission(profileId, "app:create") // 无权限抛 403 +``` + --- ## SimApiOptions 完整配置 ```cangjie builder.addSimApi { options => - options.redisConfiguration = "localhost:6379" // Redis(可选) + options.redisConfiguration = "localhost:6379" // Redis(可选,支持 ,password=xxx,db=2) // 功能开关 options.enableSimApiAuth = false // Token 认证 @@ -233,14 +399,16 @@ builder.addSimApi { options => options.enableSimApiException = true // 全局异常拦截 options.enableSimApiResponseFilter = true // 响应统一封装 options.enableSimApiHttpClient = false // HTTP 客户端 + options.enableSimApiAuthGate = false // 认证中心 SDK + 网关中间件 options.enableRequestLog = false // 请求日志中间件 options.enableCors = true // 全量 CORS options.enableLogger = true // 控制台日志 // .NET 风格子模块配置回调(对齐 C# ConfigureSimApiXxx) options.configureSimApiRoute { route => - route.userInfoRoute = Some("user/info") - route.logoutRoute = Some("auth/logout") + route.userInfoRoute = Some("/user/info") // 内置路由自定义路径 + route.logoutRoute = Some("/auth/logout") + route.webConfigRoute = Some("/config") } options.configureSimApiRequestLog { opt => opt.showFullResponse = true @@ -252,56 +420,57 @@ builder.addSimApi { options => http.appKey = "your-app-key" http.server = "https://api.example.com" } + options.configureSimApiAuthCenter { auth => + auth.server = "https://auth.example.com" + auth.appId = "auth-app-id" + auth.appKey = "auth-app-key" + } } ``` ## 内置控制器(MVC 写法) -simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),宿主通过 `addControllers` + 手动 `AssemblyPart` 注册(当前 cjc 无法自动扫描包子包): +simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描,见 `SimApiControllerScanner`): | 控制器 | 路由 | 说明 | |--------|------|------| -| `SimApiCommonController` | `/exception/{code}`、`/webconfig`、`/user/info` | 通用内置路由 | +| `SimApiCommonController` | `/exception/{code}`、`/config`、`/versions`、`/user/info` | 通用内置路由 | | `SimApiAuthController` | `/auth/logout` | 退出登录 | -| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` | +| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` | ```cangjie import simapi.controllers.* +import simapi.attributes.{SimApiAuth} // 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入 +@SimApiAuth // 类级鉴权(可选,替代 requireLogin) public class MyController <: SimApiBaseController { private let _auth: SimApiAuth public init(auth: SimApiAuth) { this._auth = auth } @HttpPost["my/route"] - public func myAction(@FromBody request: MyRequest): ApiResult { - requireLogin() - ApiResult.ok() + public func myAction(@FromBody request: MyRequest): String { + "ok" } } - -// 宿主注册 -let mvc = builder.services.addControllers() -mvc.addApplicationPart(AssemblyPart("simapi.controllers", [ - TypeInfo.of(), - TypeInfo.of(), -])) ``` +宿主无需手动注册控制器:`builder.addSimApi {}` 内部自动扫描并注册。 + --- ## 未实现模块(选项占位) -以下 C# 原包功能因仓颉生态暂无对应库,**选项保留但未实现**: +以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/MinIO/Swashbuckle),**选项保留但未实现**: | 选项 | 原功能 | 状态 | |------|--------|------| -| `enableSimApiDoc` | Swagger 文档 | ❌ 未实现 | +| `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi) | ❌ 未实现 | | `enableSimApiStorage` | S3/MinIO 存储 | ❌ 未实现 | | `enableSynapse` | MQTT 通信 | ❌ 未实现 | | `enableJob` | Hangfire 任务调度 | ❌ 未实现 | -| `enableSimApiAuthGate` | Auth Center 网关鉴权 | ❌ 未实现 | -| `SimApiAesUtil` | AES-256-CBC | ⚠️ 仓颉 std 无 AES,暂用 Base64 占位 | + +> ✅ 已实现(曾为占位):`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。 --- diff --git a/cjpm.lock b/cjpm.lock index ac90770..2acd944 100644 --- a/cjpm.lock +++ b/cjpm.lock @@ -2,19 +2,19 @@ version = 0 [requires] soulsoft_extensions_hosting = {version = "1.0.20260528"} - soulsoft_web_http = {version = "1.0.20260528"} - soulsoft_web_routing = {version = "1.0.20260528"} - soulsoft_web_cors = {version = "1.0.20260528"} - soulsoft_web_hosting = {version = "1.0.20260528"} - soulsoft_extensions_logging = {version = "1.0.20260528"} - soulsoft_web_mvc = {version = "1.0.20260528"} - soulsoft_extensions_logging_console = {version = "1.0.20260528"} - soulsoft_extensions_options = {version = "1.0.20260528"} - soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} - soulsoft_serialization = {version = "1.0.20260528"} - soulsoft_extensions_configuration = {version = "1.0.20260528"} soulsoft_extensions_options_configuration = {version = "1.0.20260528"} - soulsoft_extensions_injection = {version = "1.0.20260528"} soulsoft_net_http = {version = "1.0.20260528"} redis = {version = "1.0.20260627"} + soulsoft_web_http = {version = "1.0.20260528"} soulsoft_identity_claims = {version = "1.0.20260528"} + soulsoft_web_routing = {version = "1.0.20260528"} + soulsoft_web_hosting = {version = "1.0.20260528"} + soulsoft_extensions_logging = {version = "1.0.20260528"} + soulsoft_extensions_configuration = {version = "1.0.20260528"} + soulsoft_web_mvc = {version = "1.0.20260528"} + soulsoft_extensions_logging_console = {version = "1.0.20260528"} + soulsoft_web_cors = {version = "1.0.20260528"} + soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} + soulsoft_extensions_injection = {version = "1.0.20260528"} + soulsoft_serialization = {version = "1.0.20260528"} + soulsoft_extensions_options = {version = "1.0.20260528"}