KairoCore 1.0.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of KairoCore might be problematic. Click here for more details.
- KairoCore/__init__.py +9 -2
- KairoCore/app.py +7 -2
- KairoCore/common/errors.py +39 -1
- KairoCore/docs/CodeGenerateDoc.md +58 -0
- KairoCore/docs/FileUploadDoc.md +142 -0
- KairoCore/docs/HttpSessionDoc.md +170 -0
- KairoCore/docs/TokenUseDoc.md +349 -0
- KairoCore/docs/UseDoc.md +174 -0
- KairoCore/example/your_project_name/action/api_key_admin.py +42 -0
- KairoCore/example/your_project_name/action/auth.py +105 -0
- KairoCore/example/your_project_name/action/file_upload.py +71 -0
- KairoCore/example/your_project_name/action/http_demo.py +64 -0
- KairoCore/example/your_project_name/action/protected_demo.py +85 -0
- KairoCore/example/your_project_name/schema/auth.py +14 -0
- KairoCore/extensions/baidu/yijian.py +0 -0
- KairoCore/utils/auth.py +629 -0
- KairoCore/utils/kc_http.py +260 -0
- KairoCore/utils/kc_upload.py +218 -0
- KairoCore/utils/panic.py +21 -1
- KairoCore/utils/router.py +19 -1
- {kairocore-1.0.0.dist-info → kairocore-1.2.1.dist-info}/METADATA +5 -1
- {kairocore-1.0.0.dist-info → kairocore-1.2.1.dist-info}/RECORD +24 -9
- {kairocore-1.0.0.dist-info → kairocore-1.2.1.dist-info}/WHEEL +0 -0
- {kairocore-1.0.0.dist-info → kairocore-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# KairoCore 权限认证使用说明
|
|
6
|
+
|
|
7
|
+
本说明文档指导你在 KairoCore 项目中使用“访问令牌 + 刷新令牌”的认证与授权能力,支持浏览器与 API 调用,扩展多租户与角色控制。内容涵盖环境配置、接口说明、调用示例、安全建议与路由集成示例。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. 功能概览
|
|
12
|
+
|
|
13
|
+
- 认证模型:短期 Access Token(JWT, HS256)+ 长期 Refresh Token
|
|
14
|
+
- 支持场景:
|
|
15
|
+
- 浏览器:HttpOnly Cookie 携带 access_token
|
|
16
|
+
- API/脚本:Authorization: Bearer {access_token}
|
|
17
|
+
- 授权能力:
|
|
18
|
+
- 多租户:令牌中携带 tid
|
|
19
|
+
- 角色控制:令牌中携带 roles,实现路由级角色检查
|
|
20
|
+
- 额外支持:
|
|
21
|
+
- API_KEY:永久有效的服务访问密钥;支持 require_api_key 与 require_access_or_api_key(二选一认证)
|
|
22
|
+
- 可扩展点:
|
|
23
|
+
- 刷新令牌存储演示为内存,生产建议切换 Redis/DB
|
|
24
|
+
- 登录示例可接入真实用户目录、密码校验、租户/角色加载
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. 项目结构与关键文件
|
|
29
|
+
|
|
30
|
+
- 工具与依赖
|
|
31
|
+
- JWT/认证工具:`/home/Coding/KairoCore/utils/auth.py`
|
|
32
|
+
- 错误常量:`/home/Coding/KairoCore/common/errors.py`
|
|
33
|
+
- 路由注册与签名强制:`/home/Coding/KairoCore/utils/router.py`
|
|
34
|
+
- 示例应用(认证相关)
|
|
35
|
+
- 认证路由:`/home/Coding/KairoCore/example/your_project_name/action/auth.py`
|
|
36
|
+
- 认证 DTO:`/home/Coding/KairoCore/example/your_project_name/schema/auth.py`
|
|
37
|
+
- 受保护接口示例:`/home/Coding/KairoCore/example/your_project_name/action/protected_demo.py`
|
|
38
|
+
- API_KEY 管理路由:`/home/Coding/KairoCore/example/your_project_name/action/api_key_admin.py`
|
|
39
|
+
- 应用启动:`/home/Coding/KairoCore/example/your_project_name/main.py`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. 环境变量配置
|
|
44
|
+
|
|
45
|
+
请在 `.env`(参考 `.env.example`)中配置:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
# JWT 基本配置
|
|
49
|
+
JWT_SECRET=your-strong-secret
|
|
50
|
+
JWT_ISS=KairoCore
|
|
51
|
+
JWT_AUD=KairoCoreClients
|
|
52
|
+
ACCESS_TOKEN_TTL_SECONDS=900
|
|
53
|
+
REFRESH_TOKEN_TTL_SECONDS=1209600
|
|
54
|
+
|
|
55
|
+
# API_KEY(永久有效的服务访问密钥)
|
|
56
|
+
# 可直接在环境变量中提供(优先级更高)或通过文件提供(更适合生产)
|
|
57
|
+
KC_API_KEY=your-api-key-here
|
|
58
|
+
KC_API_KEY_FILE=/path/to/your/api.key
|
|
59
|
+
|
|
60
|
+
# 登录密码加密上传与严格模式(可选)
|
|
61
|
+
# none:不启用加密;rsa:使用 RSA-OAEP(SHA-256);aes:使用 AES-GCM
|
|
62
|
+
LOGIN_PASSWORD_ENCRYPTION=rsa
|
|
63
|
+
# 要求前端必须加密上传密码;true 时未加密或解密失败将直接拒绝登录
|
|
64
|
+
LOGIN_PASSWORD_REQUIRE_ENCRYPTION=true
|
|
65
|
+
|
|
66
|
+
# RSA 私钥的提供方式(二选一)
|
|
67
|
+
AUTH_RSA_PRIVATE_KEY_FILE=/path/to/private_key.pem
|
|
68
|
+
# 或(不推荐生产)直接提供 PEM 文本
|
|
69
|
+
# AUTH_RSA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
|
70
|
+
# 若私钥带口令
|
|
71
|
+
AUTH_RSA_PRIVATE_KEY_PASSPHRASE=your-passphrase
|
|
72
|
+
|
|
73
|
+
# AES-GCM 共享密钥模式(单一 secret_key,不使用公私钥)
|
|
74
|
+
# 将 Base64 编码的 16/24/32 字节密钥写入此变量(对应 AES-128/192/256)
|
|
75
|
+
# 例如:LOGIN_PASSWORD_SECRET_KEY=base64_of_32_bytes_key
|
|
76
|
+
# 当 LOGIN_PASSWORD_ENCRYPTION=aes 启用,或前端以 "aes:" 前缀提交时启用
|
|
77
|
+
LOGIN_PASSWORD_SECRET_KEY=
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 4. 登录密码加密上传(RSA 与 AES 两种模式)
|
|
83
|
+
|
|
84
|
+
- 模式选择:
|
|
85
|
+
- RSA 模式:当 `LOGIN_PASSWORD_ENCRYPTION=rsa` 或前端以 `rsa:` 前缀上送时,后端使用 RSA 私钥进行 OAEP(SHA-256) 解密。
|
|
86
|
+
- AES 模式(共享密钥):当 `LOGIN_PASSWORD_ENCRYPTION=aes` 或前端以 `aes:` 前缀上送时,后端使用 AES-GCM 与 `LOGIN_PASSWORD_SECRET_KEY` 解密。密钥需 Base64 编码,原始长度为 16/24/32 字节。
|
|
87
|
+
- 严格模式:开启 `LOGIN_PASSWORD_REQUIRE_ENCRYPTION=true` 后,未加密或解密失败将直接返回登录失败(401)。
|
|
88
|
+
|
|
89
|
+
- RSA 公钥获取(仅 RSA 模式需要):
|
|
90
|
+
- GET `/example/api/auth/login_public_key`
|
|
91
|
+
- 返回:`{"public_key_pem": "-----BEGIN PUBLIC KEY-----..."}`
|
|
92
|
+
|
|
93
|
+
- AES-GCM 模式无需公钥接口:
|
|
94
|
+
- 前端与后端约定同一 `secret_key`(Base64),仅在安全环境下配置,不写入代码库。
|
|
95
|
+
|
|
96
|
+
- 前端加密负载格式:
|
|
97
|
+
- RSA 模式:前端使用公钥对 `password` 进行 RSA-OAEP(SHA-256) 加密后 Base64,建议以 `rsa:` 前缀上送,例如:
|
|
98
|
+
- `"password": "rsa:BASE64_CIPHER"`
|
|
99
|
+
- 若环境已配置 `LOGIN_PASSWORD_ENCRYPTION=rsa`,也可不加前缀(后端会尝试解密)。
|
|
100
|
+
- AES-GCM 模式:前端使用共享密钥执行 AES-GCM,加密负载支持两种格式:
|
|
101
|
+
1) 聚合:`"password": "aes:" + base64(nonce(12) + ciphertext + tag(16))`
|
|
102
|
+
2) 分段:`"password": "aes:" + nonce_b64 + ":" + cipher_b64 + ":" + tag_b64`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. 认证接口使用说明
|
|
107
|
+
|
|
108
|
+
1) 登录获取令牌
|
|
109
|
+
- 请求:
|
|
110
|
+
- POST `/example/api/auth/login`
|
|
111
|
+
- JSON 体:
|
|
112
|
+
```
|
|
113
|
+
{
|
|
114
|
+
"username": "alice",
|
|
115
|
+
"password": "123456", // 或加密后的格式(见上文)
|
|
116
|
+
"tenant_id": "t1",
|
|
117
|
+
"roles": ["user", "admin"]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
- 响应示例:
|
|
121
|
+
```
|
|
122
|
+
{
|
|
123
|
+
"access_token": "eyJhbGciOi...",
|
|
124
|
+
"access_expires_at": 1730000000,
|
|
125
|
+
"refresh_token": "eyJhbGciOi...",
|
|
126
|
+
"refresh_expires_at": 1731000000,
|
|
127
|
+
"jti": "c1a2b3...",
|
|
128
|
+
"token_type": "Bearer"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
2) 使用 access_token 访问受保护接口
|
|
133
|
+
- API 调用:
|
|
134
|
+
```
|
|
135
|
+
curl http://localhost:9140/example/api/auth/me \
|
|
136
|
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
|
137
|
+
```
|
|
138
|
+
- 浏览器场景:
|
|
139
|
+
- 在网关/前端设置 HttpOnly Cookie:`access_token=YOUR_ACCESS_TOKEN`
|
|
140
|
+
- 之后直接访问 GET `/example/api/auth/me`
|
|
141
|
+
|
|
142
|
+
3) 刷新令牌(access_token 过期时)
|
|
143
|
+
- 请求:
|
|
144
|
+
- POST `/example/api/auth/refresh`
|
|
145
|
+
- JSON 体:`{"refresh_token": "YOUR_REFRESH_TOKEN"}`
|
|
146
|
+
- 返回:新 access_token 与新 refresh_token(旧的 refresh 被撤销)
|
|
147
|
+
|
|
148
|
+
4) 登出(撤销 refresh_token)
|
|
149
|
+
- 请求:
|
|
150
|
+
- POST `/example/api/auth/logout`
|
|
151
|
+
- JSON 体:`{"refresh_token": "YOUR_REFRESH_TOKEN"}`
|
|
152
|
+
- 返回:`{"ok": true}`,之后该 refresh_token 将不可用于刷新
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 6. 普通业务路由如何启用 token 校验
|
|
157
|
+
|
|
158
|
+
由于框架对路由函数签名有约束(仅允许 query/body/file),认证上下文通过依赖与 ContextVar 注入,不改变处理函数签名。
|
|
159
|
+
|
|
160
|
+
示例:只要求 access token(类方法调用版)
|
|
161
|
+
```python
|
|
162
|
+
from fastapi import APIRouter, Depends
|
|
163
|
+
from KairoCore import kcRouter, kQuery, KairoAuth
|
|
164
|
+
|
|
165
|
+
router = kcRouter(tags=["示例"])
|
|
166
|
+
|
|
167
|
+
protected = APIRouter(dependencies=[Depends(KairoAuth.require_access_token)])
|
|
168
|
+
|
|
169
|
+
@protected.get("/hello")
|
|
170
|
+
async def hello():
|
|
171
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
172
|
+
return kQuery.to_response(
|
|
173
|
+
data={"user_id": principal.get("sub")},
|
|
174
|
+
msg="ok"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
router.include_router(protected, prefix="")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
示例:要求租户 + 角色(类方法调用版)
|
|
181
|
+
```python
|
|
182
|
+
from fastapi import APIRouter, Depends
|
|
183
|
+
from KairoCore import KairoAuth
|
|
184
|
+
|
|
185
|
+
router_admin = APIRouter(dependencies=[
|
|
186
|
+
Depends(KairoAuth.require_access_token),
|
|
187
|
+
Depends(KairoAuth.require_tenant),
|
|
188
|
+
Depends(KairoAuth.require_roles(["admin"]))
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
@router_admin.get("/admin/task")
|
|
192
|
+
async def admin_task():
|
|
193
|
+
return {"msg": "admin ok"}
|
|
194
|
+
|
|
195
|
+
router.include_router(router_admin, prefix="")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
示例:仅需 API_KEY(免登录)
|
|
199
|
+
```python
|
|
200
|
+
from fastapi import APIRouter, Depends
|
|
201
|
+
from KairoCore import KairoAuth, kcRouter, kQuery
|
|
202
|
+
|
|
203
|
+
router = kcRouter(tags=["示例"])
|
|
204
|
+
only_api_key = APIRouter(dependencies=[Depends(KairoAuth.require_api_key)])
|
|
205
|
+
|
|
206
|
+
@only_api_key.get("/ping")
|
|
207
|
+
async def ping():
|
|
208
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
209
|
+
return kQuery.to_response(data={"msg": "pong-api-key", "principal": principal})
|
|
210
|
+
|
|
211
|
+
router.include_router(only_api_key, prefix="")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
示例:access token 或 API_KEY 二选一
|
|
215
|
+
```python
|
|
216
|
+
from fastapi import APIRouter, Depends
|
|
217
|
+
from KairoCore import KairoAuth
|
|
218
|
+
|
|
219
|
+
router = kcRouter(tags=["示例"])
|
|
220
|
+
flex = APIRouter(dependencies=[Depends(KairoAuth.require_access_or_api_key)])
|
|
221
|
+
|
|
222
|
+
@flex.get("/ping")
|
|
223
|
+
async def ping():
|
|
224
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
225
|
+
return {"msg": "pong", "principal": principal}
|
|
226
|
+
|
|
227
|
+
router.include_router(flex, prefix="")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
官方示例文件:
|
|
231
|
+
- `/example/your_project_name/action/protected_demo.py`
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 7. 错误码与返回约定
|
|
236
|
+
|
|
237
|
+
统一使用 `exec_with_route_error(awaitable, PanicConst)` 包装错误,常见错误:
|
|
238
|
+
- 401 Unauthorized:
|
|
239
|
+
- KCAUTH_TOKEN_INVALID(令牌无效)
|
|
240
|
+
- KCAUTH_TOKEN_EXPIRED(令牌过期)
|
|
241
|
+
- KCAUTH_TOKEN_REVOKED(令牌被撤销)
|
|
242
|
+
- KCAUTH_REFRESH_INVALID(刷新令牌无效)
|
|
243
|
+
- KCAUTH_REFRESH_EXPIRED(刷新令牌过期)
|
|
244
|
+
- 403 Forbidden:
|
|
245
|
+
- KCAUTH_TENANT_REQUIRED(需要租户信息)
|
|
246
|
+
- KCAUTH_ROLE_REQUIRED(需要角色权限)
|
|
247
|
+
- KCAUTH_PERMISSION_DENIED(权限不足)
|
|
248
|
+
- 500 Internal Server Error:
|
|
249
|
+
- KCAUTH_CONFIG_ERROR(认证配置错误,如缺少必要环境变量)
|
|
250
|
+
|
|
251
|
+
返回数据统一使用 `kQuery.to_response(...)`,包含 data、msg 等字段,便于前端消费。
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 8. 安全建议与最佳实践
|
|
256
|
+
|
|
257
|
+
- 使用 HttpOnly Cookie 存储 access_token(浏览器场景),并设置:
|
|
258
|
+
- Secure(仅 HTTPS)
|
|
259
|
+
- SameSite(Lax/Strict,依据跨站策略)
|
|
260
|
+
- 将 refresh_token 仅用于后端通道,不在前端暴露(后端刷新流程)
|
|
261
|
+
- 切换刷新令牌存储为 Redis/DB,并启用刷新轮换与黑名单
|
|
262
|
+
- 配置合理的 TTL:
|
|
263
|
+
- access_token 15–30 分钟
|
|
264
|
+
- refresh_token 7–30 天(视安全策略)
|
|
265
|
+
- 严格校验 ISS/AUD 与签名秘钥
|
|
266
|
+
- 管理角色分配,避免过宽权限(如滥发 admin)
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 9. 常见问题与排查
|
|
271
|
+
|
|
272
|
+
- 登录成功,但访问 /me 返回 403?
|
|
273
|
+
- 检查是否缺少租户 tid(require_tenant)
|
|
274
|
+
- 检查是否缺少必要角色(require_roles)
|
|
275
|
+
- 刷新失败返回 401?
|
|
276
|
+
- 可能刷新令牌已撤销或过期;检查 refresh_token 是否正确且仍有效
|
|
277
|
+
- 开发阶段需快捷获取 access_token?
|
|
278
|
+
- 可通过 login 接口获取;如需“直接发放”临时接口,请明确说明需求与安全边界(建议在 dev 环境开启,生产禁用)
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 10. 端到端测试步骤
|
|
283
|
+
|
|
284
|
+
1) 启动服务:
|
|
285
|
+
```
|
|
286
|
+
cd /home/Coding/KairoCore/example/your_project_name
|
|
287
|
+
python main.py
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
2) 登录获取令牌:
|
|
291
|
+
```
|
|
292
|
+
curl -X POST http://localhost:9140/example/api/auth/login \
|
|
293
|
+
-H "Content-Type: application/json" \
|
|
294
|
+
-d '{"username":"alice","password":"123456","tenant_id":"t1","roles":["user","admin"]}'
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
3) 访问保护接口(示例):
|
|
298
|
+
```
|
|
299
|
+
curl http://localhost:9140/example/api/protected_demo/ping \
|
|
300
|
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
4) 刷新令牌:
|
|
304
|
+
```
|
|
305
|
+
curl -X POST http://localhost:9140/example/api/auth/refresh \
|
|
306
|
+
-H "Content-Type: application/json" \
|
|
307
|
+
-d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
5) 登出:
|
|
311
|
+
```
|
|
312
|
+
curl -X POST http://localhost:9140/example/api/auth/logout \
|
|
313
|
+
-H "Content-Type: application/json" \
|
|
314
|
+
-d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
6) 仅 API_KEY 访问示例:
|
|
318
|
+
```
|
|
319
|
+
curl http://localhost:9140/example/api/protected_demo/api-key/ping \
|
|
320
|
+
-H "X-API-Key: YOUR_API_KEY"
|
|
321
|
+
```
|
|
322
|
+
或:
|
|
323
|
+
```
|
|
324
|
+
curl "http://localhost:9140/example/api/protected_demo/api-key/ping?api_key=YOUR_API_KEY"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
7) 管理 API_KEY(仅在 KC_ENABLE_API_KEY_ADMIN=true 且 KC_ENV=development 时免登录,否则需 admin 角色 + access token):
|
|
328
|
+
- 生成:
|
|
329
|
+
```
|
|
330
|
+
curl -X POST http://localhost:9140/example/api/api_key_admin/api-key/generate
|
|
331
|
+
```
|
|
332
|
+
- 获取:
|
|
333
|
+
```
|
|
334
|
+
curl http://localhost:9140/example/api/api_key_admin/api-key
|
|
335
|
+
```
|
|
336
|
+
- 删除:
|
|
337
|
+
```
|
|
338
|
+
curl -X DELETE http://localhost:9140/example/api/api_key_admin/api-key
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 11. 后续扩展与建议
|
|
344
|
+
|
|
345
|
+
- 将刷新令牌存储迁移至 Redis/DB,并提供封装适配层
|
|
346
|
+
- 登录接入真实用户目录(数据库/LDAP),并加载租户与角色
|
|
347
|
+
- 提供登录接口的浏览器版本(返回 Set-Cookie),用于前端直接写入 HttpOnly Cookie
|
|
348
|
+
- 增加更细粒度的租户资源权限模型与策略评估
|
|
349
|
+
- 单点登出(注销所有会话),以及设备/会话管理
|
KairoCore/docs/UseDoc.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# KairoCore 项目结构详细说明
|
|
2
|
+
|
|
3
|
+
## 📁 项目整体结构
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
your_project_name/
|
|
7
|
+
├── main.py # 应用程序入口文件
|
|
8
|
+
├── action/ # 业务逻辑层目录
|
|
9
|
+
│ └── user.py # 用户相关业务逻辑实现
|
|
10
|
+
├── dao/ # 数据访问层目录
|
|
11
|
+
│ ├── __init__.py # DAO模块初始化文件
|
|
12
|
+
│ └── user.py # 用户数据访问对象实现
|
|
13
|
+
├── domain/ # 领域模型层目录
|
|
14
|
+
│ ├── __init__.py # 领域模型初始化文件
|
|
15
|
+
│ └── user.py # 用户领域模型和业务逻辑处理
|
|
16
|
+
├── schema/ # 数据模式层目录
|
|
17
|
+
│ └── user.py # 用户数据传输对象和验证规则
|
|
18
|
+
└── common/ # 公共组件目录
|
|
19
|
+
├── consts.py # 全局常量定义
|
|
20
|
+
└── errors.py # 自定义异常和错误处理
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 🎯 各层详细说明
|
|
24
|
+
|
|
25
|
+
### 1. main.py - 应用入口层
|
|
26
|
+
|
|
27
|
+
**职责**:应用程序的启动文件,负责初始化和配置整个 KairoCore 应用。
|
|
28
|
+
|
|
29
|
+
**功能**:
|
|
30
|
+
- 加载环境变量配置
|
|
31
|
+
- 初始化 KairoCore 应用实例
|
|
32
|
+
- 启动应用服务器
|
|
33
|
+
- 注册路由和中间件
|
|
34
|
+
|
|
35
|
+
### 2. action/ - 业务逻辑层
|
|
36
|
+
|
|
37
|
+
**职责**:处理具体的业务逻辑,协调各组件完成业务操作
|
|
38
|
+
|
|
39
|
+
**特点**:
|
|
40
|
+
- 对接 API 接口与领域模型
|
|
41
|
+
- 实现具体的业务流程
|
|
42
|
+
- 调用 DAO 层进行数据操作
|
|
43
|
+
- 返回标准化的响应格式
|
|
44
|
+
|
|
45
|
+
**包含内容**:
|
|
46
|
+
- 定义 RESTful API 路由
|
|
47
|
+
- 处理 HTTP 请求和响应
|
|
48
|
+
- 参数验证和错误处理
|
|
49
|
+
- 用户管理的增删改查操作
|
|
50
|
+
|
|
51
|
+
### 3. schema/ - 数据模式层
|
|
52
|
+
|
|
53
|
+
**职责**:定义数据传输对象(DTO)和数据验证规则
|
|
54
|
+
|
|
55
|
+
**特点**:
|
|
56
|
+
- 使用 Pydantic 进行数据建模和验证
|
|
57
|
+
- 定义请求和响应的数据结构
|
|
58
|
+
- 包含字段验证规则和业务约束
|
|
59
|
+
- 支持枚举类型和复杂数据结构
|
|
60
|
+
|
|
61
|
+
**包含内容**:
|
|
62
|
+
- 用户数据的增删改查 DTO
|
|
63
|
+
- 数据验证规则和自定义验证器
|
|
64
|
+
- 枚举类型定义(如性别、会员状态等)
|
|
65
|
+
- 分页查询参数定义
|
|
66
|
+
|
|
67
|
+
### 4. domain/ - 领域模型层
|
|
68
|
+
|
|
69
|
+
**职责**:实现核心业务逻辑和领域规则
|
|
70
|
+
|
|
71
|
+
**特点**:
|
|
72
|
+
- 包含业务实体和领域服务
|
|
73
|
+
- 实现复杂的业务规则和计算
|
|
74
|
+
- 协调 DAO 层进行数据操作
|
|
75
|
+
- 提供领域专用的工具方法
|
|
76
|
+
|
|
77
|
+
**包含内容**:
|
|
78
|
+
- 用户领域模型实现
|
|
79
|
+
- 业务逻辑处理方法
|
|
80
|
+
- 数据转换和翻译工具
|
|
81
|
+
- 领域专用的辅助函数
|
|
82
|
+
|
|
83
|
+
### 5. dao/ - 数据访问层
|
|
84
|
+
|
|
85
|
+
**职责**:负责与数据库进行交互,处理数据持久化
|
|
86
|
+
|
|
87
|
+
**特点**:
|
|
88
|
+
- 封装数据库操作细节
|
|
89
|
+
- 提供统一的数据访问接口
|
|
90
|
+
- 处理数据查询、插入、更新、删除操作
|
|
91
|
+
- 实现分页查询和复杂查询逻辑
|
|
92
|
+
|
|
93
|
+
**包含内容**:
|
|
94
|
+
- 用户数据的 CRUD 操作
|
|
95
|
+
- 分页查询实现
|
|
96
|
+
- 数据过滤和排序逻辑
|
|
97
|
+
- 数据库连接和事务管理
|
|
98
|
+
|
|
99
|
+
### 6. common/ - 公共组件层
|
|
100
|
+
|
|
101
|
+
**职责**:提供项目范围内共享的常量、工具和配置
|
|
102
|
+
|
|
103
|
+
**特点**:
|
|
104
|
+
- 全局共享的配置和常量
|
|
105
|
+
- 统一的异常处理机制
|
|
106
|
+
- 可复用的工具函数和组件
|
|
107
|
+
- 项目级别的配置管理
|
|
108
|
+
|
|
109
|
+
**包含内容**:
|
|
110
|
+
- 系统常量和枚举定义
|
|
111
|
+
- 自定义异常类型
|
|
112
|
+
- 公共工具函数
|
|
113
|
+
- 全局配置参数
|
|
114
|
+
|
|
115
|
+
## 🔗 层间关系
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
┌─────────────┐
|
|
119
|
+
│ action │ ◄── API路由和业务协调
|
|
120
|
+
└──────┬──────┘
|
|
121
|
+
│
|
|
122
|
+
┌──────▼──────┐
|
|
123
|
+
│ domain │ ◄── 核心业务逻辑实现
|
|
124
|
+
└──────┬──────┘
|
|
125
|
+
│
|
|
126
|
+
┌──────▼──────┐
|
|
127
|
+
│ dao │ ◄── 数据访问接口
|
|
128
|
+
└──────┬──────┘
|
|
129
|
+
│
|
|
130
|
+
┌──────▼──────┐
|
|
131
|
+
│ database │ ◄── 数据存储层
|
|
132
|
+
└─────────────┘
|
|
133
|
+
|
|
134
|
+
┌─────────────┐
|
|
135
|
+
│ schema │ ◄── 数据验证和传输
|
|
136
|
+
└─────────────┘
|
|
137
|
+
|
|
138
|
+
┌─────────────┐
|
|
139
|
+
│ common │ ◄── 全局共享组件
|
|
140
|
+
└─────────────┘
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 🎨 设计原则
|
|
144
|
+
|
|
145
|
+
### 分层清晰
|
|
146
|
+
- 各层职责明确,避免交叉调用
|
|
147
|
+
- 上层依赖下层,不反向依赖
|
|
148
|
+
- 通过接口进行层间通信
|
|
149
|
+
|
|
150
|
+
### 职责单一
|
|
151
|
+
- 每个模块只负责一个核心功能
|
|
152
|
+
- 便于测试和维护
|
|
153
|
+
- 降低代码耦合度
|
|
154
|
+
|
|
155
|
+
### 可扩展性
|
|
156
|
+
- 模块化设计,易于功能扩展
|
|
157
|
+
- 插件化架构,支持自定义组件
|
|
158
|
+
- 遵循开闭原则
|
|
159
|
+
|
|
160
|
+
### 可维护性
|
|
161
|
+
- 代码结构清晰,易于理解
|
|
162
|
+
- 命名规范统一
|
|
163
|
+
- 文档和注释完整
|
|
164
|
+
|
|
165
|
+
## 🚀 开发流程
|
|
166
|
+
|
|
167
|
+
1. **定义数据结构**:在 `schema/` 中定义数据传输对象
|
|
168
|
+
2. **实现数据访问**:在 `dao/` 中实现数据库操作
|
|
169
|
+
3. **编写业务逻辑**:在 `domain/` 中实现核心业务规则
|
|
170
|
+
4. **创建API接口**:在 `action/` 中定义和实现API路由
|
|
171
|
+
5. **配置共享组件**:在 `common/` 中添加常量和异常定义
|
|
172
|
+
6. **启动应用**:通过 `main.py` 启动服务
|
|
173
|
+
|
|
174
|
+
这种分层架构设计使得项目结构清晰、易于维护和扩展,符合现代 Web 应用开发的最佳实践。
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
import os
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
|
|
5
|
+
from KairoCore import kcRouter, kQuery, KairoAuth
|
|
6
|
+
|
|
7
|
+
# 文档分组到 "API_KEY管理",路径前缀将由自动注册逻辑决定(通常为 /example/api/api_key_admin/*)
|
|
8
|
+
router = kcRouter(tags=["API_KEY管理"])
|
|
9
|
+
|
|
10
|
+
# 访问控制策略:
|
|
11
|
+
# - 若 KC_ENABLE_API_KEY_ADMIN=true 且 KC_ENV=development,则允许免登录访问管理端点(用于本地开发)
|
|
12
|
+
# - 否则(生产模式或未开启开关),要求 admin 角色 + access_token
|
|
13
|
+
ENABLE_ADMIN = os.getenv("KC_ENABLE_API_KEY_ADMIN", "false").lower() == "true"
|
|
14
|
+
ENV = os.getenv("KC_ENV", "development")
|
|
15
|
+
|
|
16
|
+
if ENABLE_ADMIN and ENV == "development":
|
|
17
|
+
dependencies = [] # 开发环境免登录
|
|
18
|
+
else:
|
|
19
|
+
dependencies = [Depends(KairoAuth.require_access_token), Depends(KairoAuth.require_roles(["admin"]))]
|
|
20
|
+
|
|
21
|
+
admin_router = APIRouter(dependencies=dependencies)
|
|
22
|
+
|
|
23
|
+
@admin_router.get("/api-key")
|
|
24
|
+
async def get_api_key() -> Dict[str, Any]:
|
|
25
|
+
"""查看当前 API_KEY(如果存在)。生产环境请谨慎暴露。"""
|
|
26
|
+
key = KairoAuth.get_api_key()
|
|
27
|
+
return kQuery.to_response(data={"api_key": key}, msg="ok")
|
|
28
|
+
|
|
29
|
+
@admin_router.post("/api-key/generate")
|
|
30
|
+
async def generate_api_key() -> Dict[str, Any]:
|
|
31
|
+
"""生成新的 API_KEY 并保存(覆盖旧值)。"""
|
|
32
|
+
key = KairoAuth.generate_api_key()
|
|
33
|
+
return kQuery.to_response(data={"api_key": key}, msg="generated")
|
|
34
|
+
|
|
35
|
+
@admin_router.delete("/api-key")
|
|
36
|
+
async def delete_api_key() -> Dict[str, Any]:
|
|
37
|
+
"""删除已存在的 API_KEY。"""
|
|
38
|
+
KairoAuth.delete_api_key()
|
|
39
|
+
return kQuery.to_response(data={"deleted": True}, msg="deleted")
|
|
40
|
+
|
|
41
|
+
# 将子路由挂载到主路由
|
|
42
|
+
router.include_router(admin_router, prefix="")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from ...schema.auth import LoginBody, RefreshBody, LogoutBody
|
|
5
|
+
from KairoCore import exec_with_route_error, kcRouter, kQuery, KairoAuth
|
|
6
|
+
from KairoCore.common.errors import (
|
|
7
|
+
KCAUTH_LOGIN_FAILED,
|
|
8
|
+
KCAUTH_REFRESH_INVALID,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
router = kcRouter(tags=["认证"])
|
|
12
|
+
|
|
13
|
+
@router.get("/login_public_key")
|
|
14
|
+
async def login_public_key() -> Dict[str, Any]:
|
|
15
|
+
"""返回用于前端加密登录密码的 RSA 公钥(如已配置)。"""
|
|
16
|
+
pem = KairoAuth.get_rsa_public_key_pem()
|
|
17
|
+
if not pem:
|
|
18
|
+
return kQuery.to_response(data=None, msg="RSA 未配置")
|
|
19
|
+
return kQuery.to_response(data={"public_key_pem": pem}, msg="ok")
|
|
20
|
+
|
|
21
|
+
@router.post("/login")
|
|
22
|
+
async def login(body: LoginBody) -> Dict[str, Any]:
|
|
23
|
+
async def _do():
|
|
24
|
+
# 解密加密上传的密码(如启用 RSA 或以前缀标识)。未配置则原样返回。
|
|
25
|
+
decrypted_password = KairoAuth.decrypt_password_if_encrypted(body.password)
|
|
26
|
+
|
|
27
|
+
# 演示用途:这里应替换为真实的用户校验与租户/角色加载
|
|
28
|
+
if not body.username or not decrypted_password:
|
|
29
|
+
raise KCAUTH_LOGIN_FAILED
|
|
30
|
+
|
|
31
|
+
roles = body.roles or ["user"]
|
|
32
|
+
access_token, access_exp = KairoAuth.issue_access_token(
|
|
33
|
+
user_id=body.username, tenant_id=body.tenant_id, roles=roles
|
|
34
|
+
)
|
|
35
|
+
refresh_token, jti, refresh_exp = KairoAuth.issue_refresh_token(
|
|
36
|
+
user_id=body.username, tenant_id=body.tenant_id
|
|
37
|
+
)
|
|
38
|
+
return {
|
|
39
|
+
"access_token": access_token,
|
|
40
|
+
"access_expires_at": access_exp,
|
|
41
|
+
"refresh_token": refresh_token,
|
|
42
|
+
"refresh_expires_at": refresh_exp,
|
|
43
|
+
"jti": jti,
|
|
44
|
+
"token_type": "Bearer",
|
|
45
|
+
}
|
|
46
|
+
return await exec_with_route_error(_do(), KCAUTH_LOGIN_FAILED)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/refresh")
|
|
50
|
+
async def refresh(body: RefreshBody) -> Dict[str, Any]:
|
|
51
|
+
async def _do():
|
|
52
|
+
payload = KairoAuth.verify_refresh_token(body.refresh_token)
|
|
53
|
+
old_jti = payload.get("jti")
|
|
54
|
+
user_id = payload.get("sub")
|
|
55
|
+
tenant_id = payload.get("tid")
|
|
56
|
+
if not old_jti or not user_id:
|
|
57
|
+
raise KCAUTH_REFRESH_INVALID
|
|
58
|
+
# 轮换 refresh,重新签发 access
|
|
59
|
+
new_refresh_token, new_jti, refresh_exp = KairoAuth.rotate_refresh_token(old_jti, user_id, tenant_id)
|
|
60
|
+
roles = payload.get("roles") or ["user"] # 如需在 refresh 中携带角色,可在登录时回填
|
|
61
|
+
access_token, access_exp = KairoAuth.issue_access_token(user_id=user_id, tenant_id=tenant_id, roles=roles)
|
|
62
|
+
return {
|
|
63
|
+
"access_token": access_token,
|
|
64
|
+
"access_expires_at": access_exp,
|
|
65
|
+
"refresh_token": new_refresh_token,
|
|
66
|
+
"refresh_expires_at": refresh_exp,
|
|
67
|
+
"jti": new_jti,
|
|
68
|
+
"token_type": "Bearer",
|
|
69
|
+
}
|
|
70
|
+
return await exec_with_route_error(_do(), KCAUTH_REFRESH_INVALID)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.post("/logout")
|
|
74
|
+
async def logout(body: LogoutBody) -> Dict[str, Any]:
|
|
75
|
+
async def _do():
|
|
76
|
+
payload = KairoAuth.verify_refresh_token(body.refresh_token)
|
|
77
|
+
# 撤销当前 refresh token(登出)
|
|
78
|
+
jti = payload.get("jti")
|
|
79
|
+
if jti:
|
|
80
|
+
KairoAuth.revoke_refresh_token(jti)
|
|
81
|
+
return {"ok": True}
|
|
82
|
+
return await exec_with_route_error(_do(), KCAUTH_REFRESH_INVALID)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# 保护接口:需要 access token;并演示租户与角色要求
|
|
86
|
+
protected_router = APIRouter(
|
|
87
|
+
dependencies=[Depends(KairoAuth.require_access_token), Depends(KairoAuth.require_tenant), Depends(KairoAuth.require_roles(["user"]))]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@protected_router.get("/me")
|
|
91
|
+
async def me() -> Dict[str, Any]:
|
|
92
|
+
principal = KairoAuth.get_current_principal() or {}
|
|
93
|
+
return kQuery.to_response(
|
|
94
|
+
data={
|
|
95
|
+
"user_id": principal.get("sub"),
|
|
96
|
+
"tenant_id": principal.get("tid"),
|
|
97
|
+
"roles": principal.get("roles") or [],
|
|
98
|
+
"exp": principal.get("exp"),
|
|
99
|
+
"iat": principal.get("iat"),
|
|
100
|
+
},
|
|
101
|
+
msg="ok"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# 将受保护路由挂载到主路由(例如 /auth/me)
|
|
105
|
+
router.include_router(protected_router, prefix="")
|