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
sa_token/sync.py ADDED
@@ -0,0 +1,268 @@
1
+ """同步门面。
2
+
3
+ 本项目内核默认是异步:``await StpUtil.login()``。
4
+ WSGI 框架(Flask、Django 同步视图)和普通脚本没有事件循环,
5
+ 用这里的 ``StpUtilSync`` 把同一套 :class:`~sa_token.stp_logic.StpLogic`
6
+ 放到后台 loop 里跑,语义与异步版完全一致。
7
+
8
+ ``Sync`` = English *synchronous*(同步),不是 asyncio / 异步。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import threading
15
+ from collections.abc import Coroutine
16
+ from typing import Any, TypeVar
17
+
18
+ from .model import TerminalInfo, TokenInfo
19
+ from .permission import MatchMode
20
+ from .security import LoginTokenPair
21
+ from .session import SaSession
22
+ from .stp_util import StpUtil
23
+
24
+ __all__ = ["run_sync", "StpUtilSync", "shutdown_sync_loop"]
25
+
26
+ _T = TypeVar("_T")
27
+
28
+ _loop: asyncio.AbstractEventLoop | None = None
29
+ _loop_thread: threading.Thread | None = None
30
+ _loop_lock = threading.Lock()
31
+
32
+
33
+ def _get_background_loop() -> asyncio.AbstractEventLoop:
34
+ """懒启动一个后台事件循环,供同步代码复用。
35
+
36
+ 每次调用都新建 loop 的话,``MemoryStorage`` 里的 ``asyncio.Lock`` 会绑定到
37
+ 已关闭的 loop 上并报错;共用一个常驻 loop 可以规避这个问题。
38
+ """
39
+ global _loop, _loop_thread
40
+ with _loop_lock:
41
+ if _loop is not None and not _loop.is_closed():
42
+ return _loop
43
+ loop = asyncio.new_event_loop()
44
+ thread = threading.Thread(
45
+ target=loop.run_forever,
46
+ name="sa-token-sync-loop",
47
+ daemon=True,
48
+ )
49
+ thread.start()
50
+ _loop, _loop_thread = loop, thread
51
+ return loop
52
+
53
+
54
+ def shutdown_sync_loop() -> None:
55
+ """关闭后台事件循环,一般只在测试或进程退出时调用。"""
56
+ global _loop, _loop_thread
57
+ with _loop_lock:
58
+ if _loop is None:
59
+ return
60
+ _loop.call_soon_threadsafe(_loop.stop)
61
+ if _loop_thread is not None:
62
+ _loop_thread.join(timeout=5)
63
+ _loop.close()
64
+ _loop, _loop_thread = None, None
65
+
66
+
67
+ def run_sync(coro: Coroutine[Any, Any, _T]) -> _T:
68
+ """在同步代码里执行协程。
69
+
70
+ 已经处于事件循环中时直接调用会死锁,因此这里明确报错而不是悄悄挂起——
71
+ 异步环境请直接 ``await`` 异步版 API。
72
+ """
73
+ try:
74
+ asyncio.get_running_loop()
75
+ except RuntimeError:
76
+ return asyncio.run_coroutine_threadsafe(coro, _get_background_loop()).result()
77
+ coro.close()
78
+ raise RuntimeError(
79
+ "run_sync 不能在事件循环中调用,异步环境请直接 await StpUtil 的异步方法"
80
+ )
81
+
82
+
83
+ class StpUtilSync:
84
+ """``StpUtil`` 的同步版本,方法名与参数完全一致。"""
85
+
86
+ @staticmethod
87
+ def login(
88
+ login_id: Any,
89
+ *,
90
+ device: str | None = None,
91
+ timeout: int | None = None,
92
+ tag: str | None = None,
93
+ extra: dict[str, Any] | None = None,
94
+ token_value: str | None = None,
95
+ ) -> str:
96
+ return run_sync(
97
+ StpUtil.login(
98
+ login_id,
99
+ device=device,
100
+ timeout=timeout,
101
+ tag=tag,
102
+ extra=extra,
103
+ token_value=token_value,
104
+ )
105
+ )
106
+
107
+ @staticmethod
108
+ def login_with_refresh(
109
+ login_id: Any,
110
+ *,
111
+ device: str | None = None,
112
+ timeout: int | None = None,
113
+ ) -> LoginTokenPair:
114
+ return run_sync(
115
+ StpUtil.login_with_refresh(login_id, device=device, timeout=timeout)
116
+ )
117
+
118
+ @staticmethod
119
+ def refresh_access_token(refresh_token: str) -> LoginTokenPair:
120
+ return run_sync(StpUtil.refresh_access_token(refresh_token))
121
+
122
+ @staticmethod
123
+ def logout(login_id: Any, *, device: str | None = None) -> None:
124
+ run_sync(StpUtil.logout(login_id, device=device))
125
+
126
+ @staticmethod
127
+ def logout_by_token(token: str | None = None) -> None:
128
+ run_sync(StpUtil.logout_by_token(token))
129
+
130
+ @staticmethod
131
+ def kickout(login_id: Any, *, device: str | None = None) -> None:
132
+ run_sync(StpUtil.kickout(login_id, device=device))
133
+
134
+ @staticmethod
135
+ def kickout_by_token(token: str | None = None) -> None:
136
+ run_sync(StpUtil.kickout_by_token(token))
137
+
138
+ @staticmethod
139
+ def replaced(login_id: Any, *, device: str | None = None) -> None:
140
+ run_sync(StpUtil.replaced(login_id, device=device))
141
+
142
+ @staticmethod
143
+ def is_login(token: str | None = None) -> bool:
144
+ return run_sync(StpUtil.is_login(token))
145
+
146
+ @staticmethod
147
+ def check_login(token: str | None = None) -> str:
148
+ return run_sync(StpUtil.check_login(token))
149
+
150
+ @staticmethod
151
+ def get_login_id(token: str | None = None) -> str:
152
+ return run_sync(StpUtil.get_login_id(token))
153
+
154
+ @staticmethod
155
+ def get_login_id_or_none(token: str | None = None) -> str | None:
156
+ return run_sync(StpUtil.get_login_id_or_none(token))
157
+
158
+ @staticmethod
159
+ def get_token_value() -> str | None:
160
+ return StpUtil.get_token_value()
161
+
162
+ @staticmethod
163
+ def get_login_id_from_context() -> str | None:
164
+ return StpUtil.get_login_id_from_context()
165
+
166
+ @staticmethod
167
+ def get_token_info(token: str | None = None) -> TokenInfo | None:
168
+ return run_sync(StpUtil.get_token_info(token))
169
+
170
+ @staticmethod
171
+ def get_offline_reason(token: str) -> dict[str, Any] | None:
172
+ return run_sync(StpUtil.get_offline_reason(token))
173
+
174
+ @staticmethod
175
+ def get_permissions(login_id: Any) -> list[str]:
176
+ return run_sync(StpUtil.get_permissions(login_id))
177
+
178
+ @staticmethod
179
+ def set_permissions(login_id: Any, permissions: list[str]) -> None:
180
+ run_sync(StpUtil.set_permissions(login_id, permissions))
181
+
182
+ @staticmethod
183
+ def has_permission(login_id: Any, permission: str) -> bool:
184
+ return run_sync(StpUtil.has_permission(login_id, permission))
185
+
186
+ @staticmethod
187
+ def check_permission(
188
+ login_id: Any,
189
+ permissions: str | list[str],
190
+ *,
191
+ mode: MatchMode = "OR",
192
+ ) -> None:
193
+ run_sync(StpUtil.check_permission(login_id, permissions, mode=mode))
194
+
195
+ @staticmethod
196
+ def get_roles(login_id: Any) -> list[str]:
197
+ return run_sync(StpUtil.get_roles(login_id))
198
+
199
+ @staticmethod
200
+ def set_roles(login_id: Any, roles: list[str]) -> None:
201
+ run_sync(StpUtil.set_roles(login_id, roles))
202
+
203
+ @staticmethod
204
+ def has_role(login_id: Any, role: str) -> bool:
205
+ return run_sync(StpUtil.has_role(login_id, role))
206
+
207
+ @staticmethod
208
+ def check_role(login_id: Any, roles: str | list[str], *, mode: MatchMode = "OR") -> None:
209
+ run_sync(StpUtil.check_role(login_id, roles, mode=mode))
210
+
211
+ @staticmethod
212
+ def get_session(login_id: Any, *, create: bool = True) -> SaSession | None:
213
+ return run_sync(StpUtil.get_session(login_id, create=create))
214
+
215
+ @staticmethod
216
+ def get_token_session(token: str | None = None) -> SaSession | None:
217
+ return run_sync(StpUtil.get_token_session(token))
218
+
219
+ @staticmethod
220
+ def disable(login_id: Any, seconds: int, *, service: str = "login", level: int = 1) -> None:
221
+ run_sync(StpUtil.disable(login_id, seconds, service=service, level=level))
222
+
223
+ @staticmethod
224
+ def untie(login_id: Any, *, service: str = "login") -> None:
225
+ run_sync(StpUtil.untie(login_id, service=service))
226
+
227
+ @staticmethod
228
+ def is_disable(login_id: Any, *, service: str = "login", level: int = 1) -> bool:
229
+ return run_sync(StpUtil.is_disable(login_id, service=service, level=level))
230
+
231
+ @staticmethod
232
+ def open_safe(token: str, business: str, seconds: int) -> None:
233
+ run_sync(StpUtil.open_safe(token, business, seconds))
234
+
235
+ @staticmethod
236
+ def check_safe(token: str | None, business: str) -> None:
237
+ run_sync(StpUtil.check_safe(token, business))
238
+
239
+ @staticmethod
240
+ def is_safe(token: str | None, business: str) -> bool:
241
+ return run_sync(StpUtil.is_safe(token, business))
242
+
243
+ @staticmethod
244
+ def get_terminal_list(login_id: Any, *, device: str | None = None) -> list[TerminalInfo]:
245
+ return run_sync(StpUtil.get_terminal_list(login_id, device=device))
246
+
247
+ @staticmethod
248
+ def issue_nonce(subject: Any, *, purpose: str = "default") -> str:
249
+ return run_sync(StpUtil.issue_nonce(subject, purpose=purpose))
250
+
251
+ @staticmethod
252
+ def consume_nonce(nonce: str, subject: Any, *, purpose: str = "default") -> None:
253
+ run_sync(StpUtil.consume_nonce(nonce, subject, purpose=purpose))
254
+
255
+ @staticmethod
256
+ def create_temp_token(
257
+ value: Any,
258
+ timeout: int,
259
+ *,
260
+ namespace: str = "default",
261
+ ) -> str:
262
+ return run_sync(
263
+ StpUtil.create_temp_token(value, timeout, namespace=namespace)
264
+ )
265
+
266
+ @staticmethod
267
+ def consume_temp_token(token: str, *, namespace: str = "default") -> Any | None:
268
+ return run_sync(StpUtil.consume_temp_token(token, namespace=namespace))
sa_token/token_io.py ADDED
@@ -0,0 +1,66 @@
1
+ """统一的 token 读取。
2
+
3
+ 读取顺序是全局唯一的一份实现,任何框架适配层都**禁止**自己再解析一遍 Header,
4
+ 否则会出现「Gin 支持 Query 但 Echo 不支持」这类跨适配器行为不一致的老问题。
5
+
6
+ 顺序:Header → Authorization 兜底 → Cookie → Query,最后统一剥离前缀。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING: # pragma: no cover - 仅供类型检查
14
+ from .adapter.http import HttpContext
15
+ from .config import SaTokenConfig
16
+
17
+ __all__ = ["AUTH_HEADER", "read_token", "cut_token_prefix"]
18
+
19
+ AUTH_HEADER = "Authorization"
20
+
21
+
22
+ def cut_token_prefix(raw: str | None, prefix: str) -> str | None:
23
+ """剥离 ``Bearer `` 之类的前缀。
24
+
25
+ 配置了前缀时,**不带前缀的值同样接受**:很多客户端(尤其是 Cookie 与
26
+ Query 场景)不会带前缀,直接拒绝会造成大量难以排查的 401。
27
+ """
28
+ if raw is None:
29
+ return None
30
+ value = raw.strip()
31
+ if not value:
32
+ return None
33
+ if not prefix:
34
+ return value
35
+ normalized_prefix = prefix.strip()
36
+ if value.lower().startswith(normalized_prefix.lower()):
37
+ value = value[len(normalized_prefix) :].strip()
38
+ return value or None
39
+
40
+
41
+ def read_token(ctx: HttpContext, config: SaTokenConfig) -> str | None:
42
+ """按固定顺序从请求中提取 token。"""
43
+ token_name = config.token_name
44
+ prefix = config.token_prefix
45
+
46
+ if config.is_read_header:
47
+ value = cut_token_prefix(ctx.get_header(token_name), prefix)
48
+ if value:
49
+ return value
50
+ # 自定义了 token_name 时,仍然兼容标准 Authorization 头。
51
+ if token_name.lower() != AUTH_HEADER.lower():
52
+ value = cut_token_prefix(ctx.get_header(AUTH_HEADER), prefix)
53
+ if value:
54
+ return value
55
+
56
+ if config.is_read_cookie:
57
+ value = cut_token_prefix(ctx.get_cookie(token_name), prefix)
58
+ if value:
59
+ return value
60
+
61
+ if config.is_read_query:
62
+ value = cut_token_prefix(ctx.get_query(token_name), prefix)
63
+ if value:
64
+ return value
65
+
66
+ return None