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.
- sa_token/__init__.py +89 -0
- sa_token/adapter/__init__.py +24 -0
- sa_token/adapter/http.py +71 -0
- sa_token/adapter/path.py +163 -0
- sa_token/adapter/pipeline.py +97 -0
- sa_token/config.py +130 -0
- sa_token/context.py +63 -0
- sa_token/exception.py +143 -0
- sa_token/integration/__init__.py +10 -0
- sa_token/integration/django.py +131 -0
- sa_token/integration/fastapi.py +315 -0
- sa_token/integration/fastapi_oauth2.py +136 -0
- sa_token/integration/flask.py +191 -0
- sa_token/integration/starlette.py +227 -0
- sa_token/listener.py +100 -0
- sa_token/manager.py +244 -0
- sa_token/model.py +145 -0
- sa_token/oauth2/__init__.py +19 -0
- sa_token/oauth2/model.py +122 -0
- sa_token/oauth2/server.py +361 -0
- sa_token/online/__init__.py +292 -0
- sa_token/permission.py +67 -0
- sa_token/py.typed +0 -0
- sa_token/security/__init__.py +14 -0
- sa_token/security/nonce.py +93 -0
- sa_token/security/refresh.py +300 -0
- sa_token/security/temp_token.py +114 -0
- sa_token/session.py +96 -0
- sa_token/sso/__init__.py +217 -0
- sa_token/storage/__init__.py +22 -0
- sa_token/storage/base.py +66 -0
- sa_token/storage/memory.py +154 -0
- sa_token/storage/redis.py +136 -0
- sa_token/stp_interface.py +20 -0
- sa_token/stp_logic.py +911 -0
- sa_token/stp_util.py +367 -0
- sa_token/strategy/__init__.py +77 -0
- sa_token/strategy/base.py +22 -0
- sa_token/strategy/builtin.py +99 -0
- sa_token/strategy/jwt.py +72 -0
- sa_token/sync.py +268 -0
- sa_token/token_io.py +66 -0
- sa_token_python_core-0.1.1.dist-info/METADATA +756 -0
- sa_token_python_core-0.1.1.dist-info/RECORD +46 -0
- sa_token_python_core-0.1.1.dist-info/WHEEL +4 -0
- 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
|