sa-token-python-core 0.1.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.
Files changed (46) hide show
  1. sa_token/__init__.py +89 -0
  2. sa_token/adapter/__init__.py +24 -0
  3. sa_token/adapter/http.py +71 -0
  4. sa_token/adapter/path.py +163 -0
  5. sa_token/adapter/pipeline.py +97 -0
  6. sa_token/config.py +130 -0
  7. sa_token/context.py +63 -0
  8. sa_token/exception.py +143 -0
  9. sa_token/integration/__init__.py +10 -0
  10. sa_token/integration/django.py +131 -0
  11. sa_token/integration/fastapi.py +315 -0
  12. sa_token/integration/fastapi_oauth2.py +136 -0
  13. sa_token/integration/flask.py +191 -0
  14. sa_token/integration/starlette.py +227 -0
  15. sa_token/listener.py +100 -0
  16. sa_token/manager.py +244 -0
  17. sa_token/model.py +145 -0
  18. sa_token/oauth2/__init__.py +19 -0
  19. sa_token/oauth2/model.py +122 -0
  20. sa_token/oauth2/server.py +361 -0
  21. sa_token/online/__init__.py +292 -0
  22. sa_token/permission.py +67 -0
  23. sa_token/py.typed +0 -0
  24. sa_token/security/__init__.py +14 -0
  25. sa_token/security/nonce.py +93 -0
  26. sa_token/security/refresh.py +300 -0
  27. sa_token/security/temp_token.py +114 -0
  28. sa_token/session.py +96 -0
  29. sa_token/sso/__init__.py +217 -0
  30. sa_token/storage/__init__.py +22 -0
  31. sa_token/storage/base.py +66 -0
  32. sa_token/storage/memory.py +154 -0
  33. sa_token/storage/redis.py +136 -0
  34. sa_token/stp_interface.py +20 -0
  35. sa_token/stp_logic.py +911 -0
  36. sa_token/stp_util.py +367 -0
  37. sa_token/strategy/__init__.py +77 -0
  38. sa_token/strategy/base.py +22 -0
  39. sa_token/strategy/builtin.py +99 -0
  40. sa_token/strategy/jwt.py +72 -0
  41. sa_token/sync.py +268 -0
  42. sa_token/token_io.py +66 -0
  43. sa_token_python_core-0.1.1.dist-info/METADATA +756 -0
  44. sa_token_python_core-0.1.1.dist-info/RECORD +46 -0
  45. sa_token_python_core-0.1.1.dist-info/WHEEL +4 -0
  46. sa_token_python_core-0.1.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,756 @@
1
+ Metadata-Version: 2.5
2
+ Name: sa-token-python-core
3
+ Version: 0.1.1
4
+ Summary: 有状态 Token 认证与权限鉴权框架(轻量核心)。灵感来源于 Java 的 Sa-Token。全量依赖请安装 sa-token-python。
5
+ Project-URL: Homepage, https://github.com/Star-yb/sa-token-python
6
+ Project-URL: Documentation, https://github.com/Star-yb/sa-token-python
7
+ Project-URL: Source, https://github.com/Star-yb/sa-token-python
8
+ Project-URL: Issues, https://github.com/Star-yb/sa-token-python/issues
9
+ Author: sa-token-python contributors
10
+ License: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: auth,authentication,authorization,rbac,sa-token,token
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: fakeredis>=2.23; extra == 'dev'
25
+ Requires-Dist: httpx2>=0.1; extra == 'dev'
26
+ Requires-Dist: lupa>=2; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: ruff>=0.6; extra == 'dev'
30
+ Provides-Extra: django
31
+ Requires-Dist: django>=4.2; extra == 'django'
32
+ Provides-Extra: fastapi
33
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
34
+ Provides-Extra: flask
35
+ Requires-Dist: flask>=3; extra == 'flask'
36
+ Provides-Extra: full
37
+ Requires-Dist: django>=4.2; extra == 'full'
38
+ Requires-Dist: fastapi>=0.110; extra == 'full'
39
+ Requires-Dist: flask>=3; extra == 'full'
40
+ Requires-Dist: pyjwt>=2; extra == 'full'
41
+ Requires-Dist: redis>=5; extra == 'full'
42
+ Requires-Dist: starlette>=0.37; extra == 'full'
43
+ Provides-Extra: jwt
44
+ Requires-Dist: pyjwt>=2; extra == 'jwt'
45
+ Provides-Extra: redis
46
+ Requires-Dist: redis>=5; extra == 'redis'
47
+ Provides-Extra: starlette
48
+ Requires-Dist: starlette>=0.37; extra == 'starlette'
49
+ Description-Content-Type: text/markdown
50
+
51
+ # sa-token-python
52
+
53
+ <p align="center">
54
+ <img src="docs/community.png" alt="加入讨论群" width="220" />
55
+ </p>
56
+ <p align="center">扫码加入讨论群</p>
57
+
58
+ 轻量级、**有状态** 的 Python 认证鉴权框架,灵感来源于 [Sa-Token](https://sa-token.cc/)(Java)。
59
+
60
+ 同族实现:[sa-token-go](https://github.com/sa-tokens/sa-token-go) · [sa-token-rust](https://github.com/sa-tokens/sa-token-rust) · [xlt-token](https://github.com/xiaoLangtou/xlt-token)(Node.js)
61
+
62
+ **核心不依赖任何 Web 框架。** 脚本、定时任务、gRPC、FastAPI、Flask、Django 共用同一套登录 / 踢人 / 权限语义。
63
+
64
+ ```python
65
+ token = await StpUtil.login(user_id)
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 特性
71
+
72
+ - 🔐 **认证** — 登录 / 登出 / 双 Token 刷新,多端登录,踢人与顶号带明确原因
73
+ - 🛡️ **鉴权** — 权限与角色,支持通配符(`user:*`)和 AND / OR
74
+ - 🛣️ **路径鉴权** — Ant 风格规则表,可按 GET / POST / PUT / DELETE 区分
75
+ - 🧯 **防重放** — 服务端 Nonce、Refresh family 重放检测、Temp Token 原子消费
76
+ - 🚫 **封禁** — 临时 / 永久,按服务、按等级
77
+ - 🔒 **二级认证** — 已登录仍需短时确认,适合支付、改密
78
+ - 💾 **Session** — 账号级与 Token 级 KV 会话
79
+ - 🎨 **Token 风格** — uuid / random / hash / timestamp / tik / 有状态 JWT
80
+ - 📦 **存储可插拔** — Memory(开发)/ Redis(生产)/ 自定义 `SaStorage`
81
+ - 🎧 **事件** — 登录、登出、踢人、封禁等,载荷只带 token 指纹
82
+ - 🌐 **框架适配** — FastAPI 优先;Starlette / Flask / Django 基础适配
83
+ - 🎫 **OAuth2** — 授权码 + PKCE + client_credentials + introspection + 吊销
84
+ - 🔑 **SSO** — 一次性 ticket、HMAC 签名与统一登出
85
+ - 👥 **在线用户** — FastAPI WebSocket 鉴权、按设备推送、踢人即断连
86
+
87
+ ---
88
+
89
+ ## 安装
90
+
91
+ Python 3.10+。两个发行名,导入均为 `sa_token`。
92
+
93
+ ```bash
94
+ pip install sa-token-python # 全量:核心 + Redis / JWT / 全部框架适配
95
+ pip install "sa-token-python[full]" # 与上一行相同
96
+
97
+ pip install sa-token-python-core # 轻量:核心 + 内存存储
98
+ pip install "sa-token-python-core[redis]" # Redis 存储
99
+ pip install "sa-token-python-core[jwt]" # JWT token 风格
100
+ pip install "sa-token-python-core[fastapi]" # FastAPI / Starlette
101
+ pip install "sa-token-python-core[flask]" # Flask
102
+ pip install "sa-token-python-core[django]" # Django
103
+ pip install "sa-token-python-core[full]" # 与 pip install sa-token-python 相同
104
+ ```
105
+
106
+ 也可按需组合 extras,例如 `"sa-token-python-core[redis,jwt,fastapi]"`。
107
+
108
+ 从源码安装:
109
+
110
+ ```bash
111
+ pip install "git+https://github.com/Star-yb/sa-token-python.git"
112
+ pip install "sa-token-python-core[fastapi] @ git+https://github.com/Star-yb/sa-token-python.git"
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 快速开始(原生,无 Web 框架)
118
+
119
+ 这是主路径。没有中间件,没有 Request,脚本里直接可用。
120
+
121
+ ```python
122
+ import asyncio
123
+ from sa_token import SaToken, StpUtil
124
+ from sa_token.storage import MemoryStorage
125
+
126
+
127
+ async def main() -> None:
128
+ SaToken.builder().storage(MemoryStorage()).timeout(7200).build()
129
+
130
+ token = await StpUtil.login("10001", device="cli")
131
+ await StpUtil.set_permissions("10001", ["user:read", "order:*"])
132
+ await StpUtil.set_roles("10001", ["admin"])
133
+
134
+ assert await StpUtil.is_login(token)
135
+ assert await StpUtil.has_permission("10001", "order:delete") # order:* 命中
136
+
137
+ await StpUtil.kickout("10001")
138
+ assert not await StpUtil.is_login(token)
139
+
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ ```python
145
+ from sa_token.storage import MemoryStorage, RedisStorage
146
+
147
+ SaToken.builder().storage(MemoryStorage()).timeout(7200).build()
148
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(7200).build()
149
+ ```
150
+
151
+ 被踢之后拿到的是明确原因,而不是笼统的「未登录」:
152
+
153
+ ```python
154
+ from sa_token import NotLoginException, NotLoginType
155
+
156
+ try:
157
+ await StpUtil.check_login(token)
158
+ except NotLoginException as exc:
159
+ assert exc.type is NotLoginType.KICK_OUT # 还可以是 BE_REPLACED / TOKEN_TIMEOUT / ...
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 异步与同步
165
+
166
+ **内核默认是异步。** 所有核心方法都是 `async`:
167
+
168
+ ```python
169
+ token = await StpUtil.login(10001)
170
+ login_id = await StpUtil.check_login(token)
171
+ ```
172
+
173
+ FastAPI / Starlette 直接 `await`。Flask、Django 同步视图、没有事件循环的脚本,用同步桥接:
174
+
175
+ ```python
176
+ from sa_token.sync import StpUtilSync
177
+
178
+ token = StpUtilSync.login(10001) # 没有 await
179
+ login_id = StpUtilSync.check_login(token)
180
+ ```
181
+
182
+ `StpUtilSync` **不是第二份鉴权实现**,只是把同一套 `StpLogic` 放到后台事件循环里跑。
183
+
184
+ 命名约定(避免和常见缩写搞混):
185
+
186
+ | 名字 | 含义 | 使用场景 |
187
+ | --- | --- | --- |
188
+ | `StpUtil` | 异步 API(`async` / `await`) | FastAPI、脚本 `asyncio.run`、任何已有事件循环的环境 |
189
+ | `StpUtilSync` | 同步 API。`Sync` = English **synchronous**(同步) | Flask、Django 同步视图、普通函数 |
190
+
191
+ 已经在事件循环里时不要调用 `StpUtilSync`,会明确报错,请直接 `await StpUtil.xxx()`。
192
+
193
+ ---
194
+
195
+ ## 核心 API
196
+
197
+ ### 认证
198
+
199
+ ```python
200
+ token = await StpUtil.login(1000)
201
+ token = await StpUtil.login("user123", device="mobile")
202
+ token = await StpUtil.login(1000, token_value="legacy-token") # 迁移旧系统
203
+
204
+ tokens = await StpUtil.login_with_refresh(1000, device="web")
205
+ new_tokens = await StpUtil.refresh_access_token(tokens.refresh_token)
206
+
207
+ await StpUtil.is_login(token)
208
+ await StpUtil.check_login(token) # 失败抛 NotLoginException
209
+ await StpUtil.get_login_id(token)
210
+
211
+ await StpUtil.logout(1000)
212
+ await StpUtil.logout_by_token(token)
213
+ await StpUtil.kickout(1000) # 下次请求 type=KICK_OUT
214
+ await StpUtil.kickout_by_token(token) # 只踢指定终端,保留原因
215
+ await StpUtil.replaced(1000) # 下次请求 type=BE_REPLACED
216
+ await StpUtil.get_offline_reason(token)
217
+ ```
218
+
219
+ ### Nonce / Temp Token
220
+
221
+ ```python
222
+ # 服务端签发;绑定用户与业务,消费时原子删除
223
+ nonce = await StpUtil.issue_nonce("10001", purpose="change-password")
224
+ await StpUtil.consume_nonce(nonce, "10001", purpose="change-password")
225
+
226
+ # 邀请、重置密码、邮箱验证等一次性链接
227
+ temp_token = await StpUtil.create_temp_token(
228
+ {"user_id": "10001"}, 300, namespace="reset-password"
229
+ )
230
+ payload = await StpUtil.consume_temp_token(
231
+ temp_token, namespace="reset-password"
232
+ )
233
+ ```
234
+
235
+ `consume_nonce` 与 `consume_temp_token` 在并发请求中只允许一次成功。Nonce 不是
236
+ 验证码,也不能代替登录校验;它用于阻止有效请求被重复提交。
237
+
238
+ ### 权限 / 角色
239
+
240
+ ```python
241
+ await StpUtil.set_permissions(1000, ["user:read", "user:write", "admin:*"])
242
+ await StpUtil.has_permission(1000, "admin:delete") # 通配符命中
243
+ await StpUtil.has_permissions_and(1000, ["user:read", "user:write"])
244
+ await StpUtil.has_permissions_or(1000, ["admin", "super"])
245
+ await StpUtil.check_permission(1000, "order:delete") # 失败抛 NotPermissionException
246
+
247
+ await StpUtil.set_roles(1000, ["admin", "manager"])
248
+ await StpUtil.has_role(1000, "admin")
249
+ await StpUtil.check_role(1000, "admin")
250
+ ```
251
+
252
+ 已有 RBAC 表时,实现 `StpInterface`(`get_permission_list` / `get_role_list`),不要把权限再抄一份进 Redis。
253
+
254
+ ### Session / 封禁 / 二级认证
255
+
256
+ ```python
257
+ session = await StpUtil.get_session(1000)
258
+ await session.set("nickname", "alice")
259
+
260
+ await StpUtil.disable(1000, seconds=3600) # 临时
261
+ await StpUtil.disable(1000, seconds=-1) # 永久
262
+ await StpUtil.disable(1000, 3600, service="comment", level=2)
263
+ await StpUtil.untie(1000)
264
+
265
+ await StpUtil.open_safe(token, "pay", 300)
266
+ await StpUtil.check_safe(token, "pay")
267
+ await StpUtil.close_safe(token, "pay")
268
+ ```
269
+
270
+ ### 多端查询
271
+
272
+ ```python
273
+ await StpUtil.get_terminal_list(1000)
274
+ await StpUtil.get_token_value_list_by_login_id(1000, device="web")
275
+ await StpUtil.search_token_value("abc") # 管理端扫描,不要放进热路径
276
+ await StpUtil.search_session("user") # 扫描 Account-Session ID
277
+ ```
278
+
279
+ ---
280
+
281
+ ## 配置
282
+
283
+ ```python
284
+ SaToken.builder() \
285
+ .storage(MemoryStorage()) \
286
+ .token_name("Authorization") \
287
+ .timeout(86400) \
288
+ .token_style("uuid") \
289
+ .is_concurrent(True) \
290
+ .is_share(False) \
291
+ .auto_renew(True) \
292
+ .build()
293
+ ```
294
+
295
+ | 配置 | 默认 | 说明 |
296
+ | --- | --- | --- |
297
+ | `token_name` | `Authorization` | Header / Cookie / Query 键名 |
298
+ | `timeout` | `2592000` | token 有效秒数,`-1` 永久 |
299
+ | `active_timeout` | `-1` | 活跃超时,超时未访问则冻结 |
300
+ | `auto_renew` | `True` | 校验成功后续期 |
301
+ | `token_style` | `uuid` | 生成风格 |
302
+ | `token_prefix` | `Bearer ` | 读取时剥离,不带前缀也接受 |
303
+ | `is_concurrent` | `True` | 是否允许多端同时在线 |
304
+ | `is_share` | `True` | 多端是否共享同一 token |
305
+ | `max_login_count` | `12` | 最大同时在线数,`-1` 不限制 |
306
+ | `refresh_token_timeout` | `2592000` | 登录态 Refresh Token 有效期 |
307
+ | `refresh_token_rotate` | `True` | 刷新时轮转 Refresh Token |
308
+ | `refresh_token_reuse_detection` | `True` | 旧值重放时吊销整个 family |
309
+ | `nonce_timeout` | `60` | 服务端 Nonce 有效秒数 |
310
+ | `is_write_cookie` | `False` | FastAPI 显式调用写 Cookie 助手时是否生效 |
311
+ | `storage_key_prefix` | `satoken:` | 所有存储键前缀 |
312
+ | `jwt_secret_key` | `None` | JWT 风格时必填 |
313
+
314
+ Token 读取顺序全局唯一:**Header → Authorization 兜底 → Cookie → Query**。各框架禁止自己再解析一遍。
315
+
316
+ ### 多端登录语义
317
+
318
+ | `is_concurrent` | `is_share` | 行为 |
319
+ | --- | --- | --- |
320
+ | `False` | 忽略 | 新登录顶掉旧登录(银行 App) |
321
+ | `True` | `True` | 同设备类型复用同一 token |
322
+ | `True` | `False` | 每端独立 token,互不影响 |
323
+
324
+ ---
325
+
326
+ ## 路径鉴权(按 HTTP 方法区分)
327
+
328
+ Java 里可以 `SaRouter.match(SaHttpMethod.POST, "/user/**")`。本项目同样支持:规则表里用 `methods=`,匹配时同时看路径和请求方法。
329
+
330
+ `methods` 省略或空列表 = 匹配该路径的**所有方法**。
331
+
332
+ ```python
333
+ from sa_token.adapter import PathAuthConfig
334
+ from sa_token.integration.fastapi import SaTokenMiddleware
335
+
336
+ path_auth = (
337
+ PathAuthConfig()
338
+ .ignore("/login", "/docs", "/openapi.json")
339
+ .ignore("/article", methods=["GET"]) # 公开阅读
340
+ .permission("/article", "article:write", methods=["POST", "PUT", "DELETE"])
341
+ .permission("/order/**", "order:*", methods=["DELETE"])
342
+ .role("/admin/**", "admin")
343
+ .login("/**") # 其余全部要登录
344
+ )
345
+
346
+ app.add_middleware(SaTokenMiddleware, path_auth=path_auth)
347
+ ```
348
+
349
+ 对应关系:
350
+
351
+ | Java | 本项目 |
352
+ | --- | --- |
353
+ | `SaRouter.match("/user/**").check(r -> StpUtil.checkLogin())` | `.login("/user/**")` |
354
+ | `SaRouter.match(SaHttpMethod.GET, "/article")` | `.ignore("/article", methods=["GET"])` 或 `.login(..., methods=["GET"])` |
355
+ | `SaRouter.match(SaHttpMethod.POST, "/article").check(perm)` | `.permission("/article", "article:write", methods=["POST"])` |
356
+
357
+ 注意:`ignore` 优先级最高。如果写成 `.ignore("/article")`(不限方法),后面的 `.permission("/article", ..., methods=["POST"])` 对 POST 也不会生效。要「GET 公开、POST 鉴权」,必须给 ignore 也加上 `methods=["GET"]`。
358
+
359
+ 路径匹配是 Ant 风格:`*` 单层,`**` 任意层。
360
+
361
+ 装饰器 / Depends 场景不需要规则表——路由本身已经绑定了方法(`@app.delete` 只对 DELETE 生效)。规则表适合网关、不想给每个路由挂注解的情况。
362
+
363
+ ---
364
+
365
+ ## FastAPI(主要适配框架)
366
+
367
+ 各框架统一用注解鉴权。FastAPI 的标准写法与 Flask / Django 相同:
368
+
369
+ ```python
370
+ from fastapi import FastAPI, Response
371
+ from sa_token import SaToken, StpUtil
372
+ from sa_token.storage import MemoryStorage, RedisStorage
373
+ from sa_token.integration.fastapi import SaTokenFastAPI, set_token_cookie
374
+
375
+ SaToken.builder().storage(MemoryStorage()).set_option(is_write_cookie=True).build()
376
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).set_option(is_write_cookie=True).build()
377
+
378
+ app = FastAPI()
379
+ sa = SaTokenFastAPI(app)
380
+
381
+
382
+ @app.post("/login")
383
+ async def login(user_id: str, response: Response) -> dict:
384
+ tokens = await StpUtil.login_with_refresh(user_id, device="web")
385
+ set_token_cookie(response, tokens.access_token)
386
+ return tokens.to_dict()
387
+
388
+
389
+ @app.get("/user")
390
+ @sa.check_login
391
+ async def user_info() -> dict:
392
+ return {"id": sa.login_id()}
393
+
394
+
395
+ @app.delete("/order/{order_id}")
396
+ @sa.check_permission("order:delete")
397
+ async def delete_order(order_id: str) -> dict:
398
+ return {"deleted": order_id}
399
+ ```
400
+
401
+ 作为主要适配框架,FastAPI 额外提供 `Depends` / `Annotated`(`LoginId`、`BearerLoginId`),语义与注解相同:
402
+
403
+ ```python
404
+ from fastapi import Depends
405
+ from sa_token.integration.fastapi import LoginId, BearerLoginId, check_permission
406
+
407
+
408
+ @app.get("/profile")
409
+ async def profile(login_id: LoginId) -> dict:
410
+ return {"id": login_id}
411
+
412
+
413
+ @app.get("/me")
414
+ async def me(login_id: BearerLoginId) -> dict:
415
+ return {"id": login_id}
416
+
417
+
418
+ @app.get("/order/{order_id}")
419
+ async def get_order(order_id: str, _: str = Depends(check_permission("order:read"))) -> dict:
420
+ return {"id": order_id}
421
+ ```
422
+
423
+ ### FastAPI WebSocket
424
+
425
+ ```python
426
+ from fastapi import WebSocket
427
+ from sa_token.integration.fastapi import authenticate_websocket
428
+
429
+ @app.websocket("/ws")
430
+ async def websocket_endpoint(websocket: WebSocket) -> None:
431
+ login_id, token = await authenticate_websocket(websocket)
432
+ await websocket.accept()
433
+ ```
434
+
435
+ 浏览器 WebSocket 不能自定义 Header 时可使用
436
+ `/ws?Authorization=<token>`。生产代码应再用 `OnlineManager.register` 登记连接,
437
+ 并在心跳、断开时调用 `heartbeat` / `unregister`。
438
+
439
+ 完整的登录刷新、Cookie、权限、Nonce、WebSocket 示例见
440
+ [`examples/fastapi/main.py`](examples/fastapi/main.py)。
441
+
442
+ ---
443
+
444
+ ## Flask
445
+
446
+ Flask 没有事件循环,用 `StpUtilSync`。鉴权与 FastAPI、Django 一样使用注解。
447
+
448
+ ```python
449
+ from flask import Flask
450
+ from sa_token import SaToken
451
+ from sa_token.storage import MemoryStorage, RedisStorage
452
+ from sa_token.integration.flask import SaTokenFlask
453
+ from sa_token.sync import StpUtilSync
454
+
455
+ SaToken.builder().storage(MemoryStorage()).build()
456
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).build()
457
+
458
+ app = Flask(__name__)
459
+ sa = SaTokenFlask(app)
460
+
461
+
462
+ @app.post("/login")
463
+ def login():
464
+ return {"token": StpUtilSync.login("10001")}
465
+
466
+
467
+ @app.get("/user")
468
+ @sa.check_login
469
+ def user_info():
470
+ return {"id": sa.login_id()}
471
+
472
+
473
+ @app.delete("/order/<order_id>")
474
+ @sa.check_permission("order:delete")
475
+ def delete_order(order_id):
476
+ return {"deleted": order_id}
477
+ ```
478
+
479
+ 完整示例见 [`examples/flask/main.py`](examples/flask/main.py)。
480
+
481
+ ---
482
+
483
+ ## Django
484
+
485
+ Django 自带 `django.contrib.auth`(Session + User 模型)。本库**不是**去替换它,而是给 **纯 API / DRF** 项目提供与 FastAPI、Flask 一致的 Token 鉴权。模板站点继续用 Django Auth 即可。
486
+
487
+ 安装:`pip install "sa-token-python-core[django]"` 或 `pip install sa-token-python`。Django 是同步 WSGI,一律走 `StpUtilSync`。
488
+
489
+ ### 1. 启动时初始化
490
+
491
+ 在某个 App 的 `AppConfig.ready()` 里构建一次 Manager(只做一次,不要在每个请求里 `build()`):
492
+
493
+ ```python
494
+ # myapp/apps.py
495
+ from django.apps import AppConfig
496
+
497
+ class MyappConfig(AppConfig):
498
+ name = "myapp"
499
+
500
+ def ready(self) -> None:
501
+ from sa_token import SaToken
502
+ from sa_token.storage import MemoryStorage, RedisStorage
503
+
504
+ SaToken.builder().storage(MemoryStorage()).timeout(86400).print_banner(False).build()
505
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).print_banner(False).build()
506
+ ```
507
+
508
+ ### 2. 注册中间件
509
+
510
+ ```python
511
+ # settings.py
512
+ MIDDLEWARE = [
513
+ # ... Django 自带中间件 ...
514
+ "sa_token.integration.django.SaTokenDjangoMiddleware",
515
+ ]
516
+ ```
517
+
518
+ 默认行为与 FastAPI 中间件相同:**只解析 token**,挂到 `request.sa_token` / `request.sa_login_id`,不强制登录。未登录访问公开接口不会 401。
519
+
520
+ 鉴权异常会被中间件翻成 JSON:`{"code": 401, "message": "...", "error": "NotLoginException", "type": "NOT_TOKEN"}`。
521
+
522
+ ### 3. 登录 / 登出视图
523
+
524
+ ```python
525
+ from django.http import JsonResponse
526
+ from django.views.decorators.csrf import csrf_exempt
527
+ from sa_token.sync import StpUtilSync
528
+
529
+ @csrf_exempt
530
+ def login(request):
531
+ # 这里自己查数据库校验账号密码,通过后:
532
+ user_id = "10001"
533
+ token = StpUtilSync.login(user_id, device="web")
534
+ StpUtilSync.set_permissions(user_id, ["user:read", "order:*"])
535
+ StpUtilSync.set_roles(user_id, ["admin"])
536
+ return JsonResponse({"token": token})
537
+
538
+ def logout(request):
539
+ StpUtilSync.logout_by_token(request.sa_token)
540
+ return JsonResponse({"ok": True})
541
+ ```
542
+
543
+ 客户端之后在 Header 带 `Authorization: Bearer <token>`。Cookie / Query 同样能读到(由核心 `token_io` 统一处理)。
544
+
545
+ ### 4. 视图装饰器
546
+
547
+ 与 FastAPI、Flask 相同,使用注解:
548
+
549
+ ```python
550
+ from django.http import JsonResponse
551
+ from sa_token.integration.django import check_login, check_permission, check_role
552
+
553
+ @check_login
554
+ def me(request):
555
+ return JsonResponse({"login_id": request.sa_login_id})
556
+
557
+ @check_permission("order:delete")
558
+ def delete_order(request, order_id):
559
+ return JsonResponse({"deleted": order_id})
560
+
561
+ @check_role("admin")
562
+ def admin_panel(request):
563
+ return JsonResponse({"ok": True})
564
+
565
+ @check_permission("user:read", "user:write", mode="AND")
566
+ def update_user(request):
567
+ return JsonResponse({"ok": True})
568
+ ```
569
+
570
+ 装饰器失败时直接返回 401 / 403 JSON,不会进到视图函数。
571
+
572
+ ### 5. 路径规则表(可选)
573
+
574
+ 不想给每个 view 挂装饰器时,在初始化之后给模块级 `PATH_AUTH` 赋值,中间件就会按规则表强制鉴权:
575
+
576
+ ```python
577
+ # 建议放在 AppConfig.ready() 里,build() 之后
578
+ from sa_token.adapter import PathAuthConfig
579
+ from sa_token.integration import django as sa_django
580
+
581
+ sa_django.PATH_AUTH = (
582
+ PathAuthConfig()
583
+ .ignore("/api/login", "/api/health")
584
+ .ignore("/api/article", methods=["GET"])
585
+ .permission("/api/article", "article:write", methods=["POST", "PUT", "DELETE"])
586
+ .role("/api/admin/**", "admin")
587
+ .login("/api/**")
588
+ )
589
+ ```
590
+
591
+ ### 6. 与 DRF 一起用
592
+
593
+ 中间件仍然生效。DRF ViewSet 上继续套同一个装饰器,或在 `initial()` / permission 类里调用 `StpUtilSync.check_login(request.sa_token)`。不要在 DRF 里再写一套 Header 解析。
594
+
595
+ 取当前用户:
596
+
597
+ ```python
598
+ login_id = request.sa_login_id # 中间件或装饰器已经校验过时
599
+ # 或
600
+ login_id = StpUtilSync.get_login_id(request.sa_token)
601
+ ```
602
+
603
+ ### 7. 不要和 Django Auth 混用同一套会话
604
+
605
+ | | Django Auth | sa-token-python |
606
+ | --- | --- | --- |
607
+ | 凭证 | `sessionid` Cookie | Token(Header / Cookie / Query) |
608
+ | 用户模型 | `AUTH_USER_MODEL` | 只认 `login_id` 字符串 |
609
+ | 踢人 | 清 session / 改密码未必立刻失效 | `kickout` 立刻生效 |
610
+ | 适用 | 模板站点、Admin | 纯 API、多端、需要踢人封禁 |
611
+
612
+ 两套可以并存(Admin 继续用 Django Auth,API 用本库),但不要让同一个接口两套都校验。
613
+
614
+ ---
615
+
616
+ ## 存储
617
+
618
+ ```python
619
+ from sa_token import SaToken
620
+ from sa_token.storage import MemoryStorage, RedisStorage
621
+
622
+ SaToken.builder().storage(MemoryStorage()).timeout(7200).build()
623
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(7200).build()
624
+ SaToken.builder().storage(RedisStorage.from_url("redis://:your_password@127.0.0.1:6379/0")).timeout(7200).build()
625
+ SaToken.builder().storage(RedisStorage.from_url("redis://user:your_password@127.0.0.1:6379/1")).timeout(7200).build()
626
+ SaToken.builder().storage(RedisStorage.from_url("rediss://:your_password@redis.example.com:6379/0")).timeout(7200).build()
627
+ ```
628
+
629
+ ```python
630
+ from redis.asyncio import Redis
631
+ from sa_token.storage import RedisStorage
632
+
633
+ client = Redis(host="127.0.0.1", port=6379, db=0, password="your_password", decode_responses=True)
634
+ SaToken.builder().storage(RedisStorage(client)).build()
635
+ ```
636
+
637
+ 自定义存储实现 `SaStorage`:`get` / `set` / `delete` / `expire` / `ttl` / `set_if_absent` / `compare_and_set` / `scan`。
638
+
639
+ ---
640
+
641
+ ## Token 风格
642
+
643
+ `uuid`(默认)、`simple-uuid`、`random32` / `random64` / `random128`、`hash`、`timestamp`、`tik`、`jwt`。
644
+
645
+ JWT 是**有状态 JWT**:token 自带 claims,校验仍查存储。踢人、顶号、封禁继续有效。不要把它理解成「签发后服务端不管」。
646
+
647
+ ```python
648
+ SaToken.builder().storage(MemoryStorage()).token_style("jwt").jwt_secret_key("至少32字节的密钥").build()
649
+ ```
650
+
651
+ ---
652
+
653
+ ## 事件
654
+
655
+ ```python
656
+ from sa_token import Event, EventData, SaToken
657
+ from sa_token.storage import MemoryStorage
658
+
659
+ def on_event(data: EventData) -> None:
660
+ print(data.event, data.login_id, data.token_fingerprint)
661
+
662
+ SaToken.builder().storage(MemoryStorage()).on(Event.ALL, on_event).build()
663
+ ```
664
+
665
+ 事件包括 `LOGIN` / `LOGOUT` / `KICKOUT` / `REPLACED` / `DISABLE` / `UNTIE` / `RENEW` 等。载荷只有 token 指纹,不会把原始 token 打进日志。监听器异常不影响主流程。
666
+
667
+ ---
668
+
669
+ ## 扩展模块
670
+
671
+ 全部框架无关,可在单测里不起 HTTP 服务跑通。
672
+
673
+ **OAuth2**(授权码 + PKCE + 原子 refresh 轮转 + client_credentials):
674
+
675
+ ```python
676
+ from sa_token.oauth2 import OAuth2Client, OAuth2Server, generate_pkce_pair
677
+
678
+ server = OAuth2Server(manager)
679
+ await server.register_client(OAuth2Client(
680
+ client_id="web-app",
681
+ client_secret="secret",
682
+ redirect_uris=["https://app.example/callback"],
683
+ scopes=["read"],
684
+ ))
685
+ code = await server.create_authorization_code(
686
+ client_id="web-app", login_id="10001", redirect_uri="https://app.example/callback",
687
+ )
688
+ tokens = await server.exchange_code_for_token(
689
+ code=code.code, client_id="web-app", client_secret="secret",
690
+ )
691
+ ```
692
+
693
+ FastAPI 可直接挂载标准 `/token`、`/introspect`、`/revoke`:
694
+
695
+ ```python
696
+ from sa_token.integration.fastapi_oauth2 import create_oauth2_router
697
+
698
+ app.include_router(create_oauth2_router(server))
699
+ ```
700
+
701
+ 授权页是否同意属于业务决策,不会由库自动放行;用户确认后调用
702
+ `authorization_redirect`。
703
+
704
+ **SSO**:配置 `SsoConfig(secret_key="...")` 后使用
705
+ `create_signed_ticket` / `validate_ticket(..., signature=...)`;ticket 一次性且绑定
706
+ service。`SsoServer.logout` 返回待通知客户端列表,HTTP 或消息队列由应用选择。
707
+
708
+ **在线用户 / WebSocket**:`OnlineManager` 支持心跳、按用户或设备推送、按设备
709
+ 断开和过期连接清理。通过 `StpUtil.kickout` / `replaced` 触发核心事件时,本进程
710
+ 连接会自动关闭。跨 worker 的连接对象不能放进 Redis,跨进程广播需由部署层接入
711
+ Redis Pub/Sub 或消息总线。
712
+
713
+ ---
714
+
715
+ ## 异常
716
+
717
+ | 异常 | HTTP | 含义 |
718
+ | --- | --- | --- |
719
+ | `NotLoginException` | 401 | 未登录 / 无效 / 超时 / 被踢 / 被顶 / 冻结,看 `exc.type` |
720
+ | `NotPermissionException` | 403 | 权限不足 |
721
+ | `NotRoleException` | 403 | 角色不足 |
722
+ | `DisableException` | 403 | 账号封禁 |
723
+ | `NotSafeException` | 403 | 二级认证未通过 |
724
+ | `SecurityException` | 400 | Nonce / Refresh / Temp Token 安全流程失败,看 `exc.code` |
725
+ | `SaTokenException` | 500 | 其它内部错误 |
726
+
727
+ `NotLoginException.type`:`NOT_TOKEN` / `INVALID_TOKEN` / `TOKEN_TIMEOUT` / `TOKEN_FREEZE` / `BE_REPLACED` / `KICK_OUT`。
728
+
729
+ 框架适配只做异常 → HTTP 的翻译,不在适配层重新发明错误码。
730
+
731
+ ---
732
+
733
+ ## 项目结构
734
+
735
+ ```
736
+ src/sa_token/
737
+ ├── stp_logic.py # 认证鉴权的唯一实现
738
+ ├── stp_util.py # 异步静态门面
739
+ ├── sync.py # 同步桥接 StpUtilSync
740
+ ├── adapter/ # HttpContext + 路径规则 + 统一管道
741
+ ├── integration/ # FastAPI / Flask / Django / Starlette(只做适配)
742
+ ├── storage/ # Memory / Redis
743
+ ├── strategy/ # Token 生成风格
744
+ ├── security/ # Nonce / 登录 Refresh / Temp Token
745
+ ├── oauth2/ sso/ online/
746
+ ```
747
+
748
+ 依赖方向只能从上到下:`integration` 可以依赖 core,core 绝不能 import FastAPI / Flask / Django。
749
+
750
+ 示例见 [examples/](examples/)。
751
+
752
+ ---
753
+
754
+ ## License
755
+
756
+ Apache-2.0