sa-token-python-core 0.1.2__py3-none-any.whl → 0.1.4__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/banner.py ADDED
@@ -0,0 +1,76 @@
1
+ """启动横幅。
2
+
3
+ 与 Java / Go 版一致:初始化时默认打印一次 ASCII 标志。
4
+ Web 框架适配会在服务启动时再尝试打印;进程内只输出一次。
5
+ ``is_print_banner=False`` 或 ``.print_banner(False)`` 可关闭。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING: # pragma: no cover - 仅供类型检查
14
+ from .manager import SaTokenManager
15
+
16
+ __all__ = ["BANNER", "emit_banner", "emit_configured_banner", "reset_banner_state"]
17
+
18
+ BANNER = r"""
19
+ _____ ______ __
20
+ / ___/____ _ /_ __/___ / /_____ ____
21
+ \__ \/ __ `/ / / / __ \/ //_/ _ \/ __ \
22
+ ___/ / /_/ / / / / /_/ / ,< / __/ / / /
23
+ /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/
24
+ """
25
+
26
+ _printed = False
27
+
28
+
29
+ def reset_banner_state() -> None:
30
+ """测试隔离:允许下一个用例再次打印。"""
31
+ global _printed
32
+ _printed = False
33
+
34
+
35
+ def render_banner(manager: SaTokenManager) -> str:
36
+ from .manager import __version__
37
+
38
+ timeout = manager.config.timeout
39
+ timeout_text = "永久" if timeout < 0 else f"{timeout} 秒"
40
+ lines = [
41
+ BANNER.rstrip("\n"),
42
+ f":: sa-token-python :: (v{__version__})",
43
+ f":: Python :: {sys.version.split()[0]}",
44
+ f":: Token Style :: {manager.config.token_style}",
45
+ f":: Token Timeout :: {timeout_text}",
46
+ f":: Storage :: {type(manager.storage).__name__}",
47
+ "",
48
+ ]
49
+ return "\n".join(lines)
50
+
51
+
52
+ def write_banner(manager: SaTokenManager) -> None:
53
+ """无条件写出横幅,供手动调用。"""
54
+ sys.stdout.write(render_banner(manager) + "\n")
55
+ sys.stdout.flush()
56
+
57
+
58
+ def emit_banner(manager: SaTokenManager) -> None:
59
+ """按配置打印横幅,同一进程只打印一次。"""
60
+ global _printed
61
+ if _printed or not manager.config.is_print_banner:
62
+ return
63
+ _printed = True
64
+ write_banner(manager)
65
+
66
+
67
+ def emit_configured_banner() -> None:
68
+ """框架启动钩子:全局 Manager 已就绪则打印。"""
69
+ from .exception import SaTokenNotInitializedException
70
+ from .stp_util import get_manager
71
+
72
+ try:
73
+ manager = get_manager()
74
+ except SaTokenNotInitializedException:
75
+ return
76
+ emit_banner(manager)
sa_token/config.py CHANGED
@@ -105,7 +105,13 @@ class SaTokenConfig:
105
105
  jwt_algorithm: str = "HS256"
106
106
 
107
107
  is_print_banner: bool = True
108
- """初始化时是否打印启动横幅。"""
108
+ """是否打印启动横幅。默认 True,与 Java / Go 版一致。
109
+
110
+ 原生 ``build()``、FastAPI / Starlette 服务启动、Flask ``init_app``、
111
+ Django 中间件加载时都会尝试打印,同一进程只输出一次。
112
+ 关闭:``SaToken.builder().print_banner(False)`` 或
113
+ ``set_option(is_print_banner=False)``。
114
+ """
109
115
 
110
116
  extra: dict[str, Any] = field(default_factory=dict)
111
117
  """给扩展模块(OAuth2 / SSO / 在线用户)放自定义配置。"""
@@ -14,6 +14,7 @@ from typing import Any, TypeVar
14
14
  from ..adapter.http import HttpContext
15
15
  from ..adapter.path import PathAuthConfig
16
16
  from ..adapter.pipeline import build_rule, resolve_token, run_auth_flow, run_path_auth
17
+ from ..banner import emit_configured_banner
17
18
  from ..exception import SaTokenException
18
19
  from ..permission import MatchMode
19
20
  from ..stp_util import get_manager
@@ -78,6 +79,7 @@ class SaTokenDjangoMiddleware:
78
79
 
79
80
  def __init__(self, get_response: Callable[[Any], Any]) -> None:
80
81
  self._get_response = get_response
82
+ emit_configured_banner()
81
83
 
82
84
  def __call__(self, request: Any) -> Any:
83
85
  ctx = DjangoHttpContext(request)
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
13
13
  from ..adapter.http import HttpContext
14
14
  from ..adapter.path import PathAuthConfig
15
15
  from ..adapter.pipeline import build_rule, resolve_token, run_auth_flow, run_path_auth
16
+ from ..banner import emit_configured_banner
16
17
  from ..exception import SaTokenException
17
18
  from ..permission import MatchMode
18
19
  from ..stp_util import get_manager
@@ -77,6 +78,9 @@ class SaTokenFlask:
77
78
  def init_app(self, app: Flask) -> None:
78
79
  app.before_request(self._before_request)
79
80
  app.register_error_handler(SaTokenException, self._handle_exception)
81
+ if hasattr(app, "before_serving"):
82
+ app.before_serving(emit_configured_banner)
83
+ emit_configured_banner()
80
84
 
81
85
  # 请求钩子 --------------------------------------------------------------
82
86
 
@@ -26,6 +26,7 @@ except ImportError as exc: # pragma: no cover - 依赖缺失路径
26
26
  from ..adapter.http import HttpContext
27
27
  from ..adapter.path import PathAuthConfig
28
28
  from ..adapter.pipeline import build_rule, resolve_token, run_auth_flow, run_path_auth
29
+ from ..banner import emit_configured_banner
29
30
  from ..exception import SaTokenException
30
31
  from ..permission import MatchMode
31
32
  from ..stp_util import get_manager
@@ -99,6 +100,7 @@ class SaTokenMiddleware(BaseHTTPMiddleware):
99
100
  super().__init__(app)
100
101
  self.path_auth = path_auth
101
102
  self.login_type = login_type
103
+ emit_configured_banner()
102
104
 
103
105
  async def dispatch(
104
106
  self,
sa_token/manager.py CHANGED
@@ -6,9 +6,9 @@ Manager 负责组装:配置 + 存储 + Token 策略 + 事件总线 + 权限数
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import sys
10
9
  from typing import TYPE_CHECKING, Any
11
10
 
11
+ from .banner import emit_banner, write_banner
12
12
  from .config import SaTokenConfig
13
13
  from .listener import Event, EventBus, Listener
14
14
  from .model import DEFAULT_LOGIN_TYPE
@@ -22,15 +22,7 @@ if TYPE_CHECKING: # pragma: no cover - 仅供类型检查
22
22
 
23
23
  __all__ = ["SaTokenManager", "SaTokenBuilder", "SaToken", "__version__"]
24
24
 
25
- __version__ = "0.1.2"
26
-
27
- _BANNER = r"""
28
- _____ ______ __
29
- / ___/____ _ /_ __/___ / /_____ ____
30
- \__ \/ __ `/ / / / __ \/ //_/ _ \/ __ \
31
- ___/ / /_/ / / / / /_/ / ,< / __/ / / /
32
- /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/
33
- """
25
+ __version__ = "0.1.4"
34
26
 
35
27
 
36
28
  class SaTokenManager:
@@ -54,6 +46,7 @@ class SaTokenManager:
54
46
  self._nonce_manager: NonceManager | None = None
55
47
  self._refresh_token_manager: RefreshTokenManager | None = None
56
48
  self._temp_token_manager: TempTokenManager | None = None
49
+ emit_banner(self)
57
50
 
58
51
  def stp(self, login_type: str = DEFAULT_LOGIN_TYPE) -> StpLogic:
59
52
  """取得指定账号体系的逻辑实例,不同 ``login_type`` 之间存储完全隔离。"""
@@ -100,16 +93,8 @@ class SaTokenManager:
100
93
  return self._temp_token_manager
101
94
 
102
95
  def print_banner(self) -> None:
103
- lines = [
104
- _BANNER,
105
- f":: sa-token-python :: (v{__version__})",
106
- f":: Python :: {sys.version.split()[0]}",
107
- f":: Token Style :: {self.config.token_style}",
108
- f":: Token Timeout :: {self.config.timeout} 秒",
109
- f":: Storage :: {type(self.storage).__name__}",
110
- "",
111
- ]
112
- print("\n".join(lines))
96
+ """手动打印启动横幅,不受「只打印一次」限制。"""
97
+ write_banner(self)
113
98
 
114
99
 
115
100
  class SaTokenBuilder:
@@ -190,6 +175,7 @@ class SaTokenBuilder:
190
175
  return self
191
176
 
192
177
  def print_banner(self, value: bool) -> SaTokenBuilder:
178
+ """是否打印启动横幅,默认 True。传入 False 关闭。"""
193
179
  self._config.is_print_banner = value
194
180
  return self
195
181
 
@@ -227,8 +213,6 @@ class SaTokenBuilder:
227
213
  stp_interface=self._stp_interface,
228
214
  events=self._events,
229
215
  )
230
- if self._config.is_print_banner:
231
- manager.print_banner()
232
216
  if self._set_global:
233
217
  from .stp_util import set_manager
234
218
 
@@ -228,15 +228,27 @@ class RefreshTokenManager:
228
228
  if cursor is None:
229
229
  return
230
230
 
231
- async def revoke_all_for_login(self, login_type: str, login_id: str) -> None:
231
+ async def revoke_all_for_login(
232
+ self,
233
+ login_type: str,
234
+ login_id: str,
235
+ *,
236
+ logout_access: bool = True,
237
+ ) -> None:
232
238
  raw = await self._storage.get(self._user_index_key(login_type, login_id))
233
239
  families = json.loads(raw) if raw else []
234
240
  if isinstance(families, list):
235
241
  for family_id in families:
236
- await self.revoke_family(str(family_id))
242
+ await self.revoke_family(str(family_id), logout_access=logout_access)
237
243
  await self._storage.delete(self._user_index_key(login_type, login_id))
238
244
 
239
- async def revoke_family(self, family_id: str) -> None:
245
+ async def revoke_family(self, family_id: str, *, logout_access: bool = True) -> None:
246
+ """吊销整个 token family。
247
+
248
+ ``logout_access=False`` 时只作废 refresh 侧记录。账号级 kickout/replaced
249
+ 已经把 access token 标成下线原因,若这里再 ``logout_by_token`` 会把刚写入
250
+ 的离线记录删掉,客户端就会看到 ``INVALID_TOKEN`` 而不是 ``KICK_OUT``。
251
+ """
240
252
  key = self._family_key(family_id)
241
253
  raw = await self._storage.get(key)
242
254
  family = _FamilyRecord.from_json(raw) if raw else None
@@ -245,8 +257,12 @@ class RefreshTokenManager:
245
257
  family.revoked = True
246
258
  await self._storage.set(key, family.to_json(), self._manager.config.refresh_token_timeout)
247
259
  logic = self._manager.stp(family.login_type)
248
- for access_token in set(family.access_tokens):
249
- await logic.logout_by_token(access_token, revoke_refresh=False)
260
+ if logout_access:
261
+ for access_token in set(family.access_tokens):
262
+ info = await logic.get_token_info(access_token)
263
+ if info is not None and info.is_offline:
264
+ continue
265
+ await logic.logout_by_token(access_token, revoke_refresh=False)
250
266
  for token in set(family.refresh_tokens):
251
267
  await self._storage.delete(self._refresh_key(token))
252
268
 
sa_token/stp_logic.py CHANGED
@@ -370,8 +370,10 @@ class StpLogic:
370
370
  offline_count = await self._offline_terminals(session, normalized_id, device, state)
371
371
  await self._save_or_drop_session(session, normalized_id)
372
372
  if device is None:
373
+ # 离线原因已经写进 access token;family 吊销只作废 refresh,不能再
374
+ # logout 把记录删掉,否则客户端会看到 INVALID_TOKEN。
373
375
  await self._manager.refresh_tokens.revoke_all_for_login(
374
- self.login_type, normalized_id
376
+ self.login_type, normalized_id, logout_access=False
375
377
  )
376
378
  if offline_count:
377
379
  await self._emit(event, login_id=normalized_id, device=device)
sa_token/stp_util.py CHANGED
@@ -29,6 +29,9 @@ def set_manager(manager: SaTokenManager) -> None:
29
29
  """设置全局 Manager,一般由 ``SaTokenBuilder.build()`` 自动调用。"""
30
30
  global _manager
31
31
  _manager = manager
32
+ from .banner import emit_banner
33
+
34
+ emit_banner(manager)
32
35
 
33
36
 
34
37
  def get_manager() -> SaTokenManager:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sa-token-python-core
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: 有状态 Token 认证与权限鉴权框架(轻量核心)。灵感来源于 Java 的 Sa-Token。常用依赖请安装 sa-token-python;Web 框架按 extras 安装。
5
5
  Project-URL: Homepage, https://github.com/Star-yb/sa-token-python
6
6
  Project-URL: Documentation, https://github.com/Star-yb/sa-token-python
@@ -313,6 +313,7 @@ SaToken.builder() \
313
313
  | `is_write_cookie` | `False` | FastAPI 显式调用写 Cookie 助手时是否生效 |
314
314
  | `storage_key_prefix` | `satoken:` | 所有存储键前缀 |
315
315
  | `jwt_secret_key` | `None` | JWT 风格时必填 |
316
+ | `is_print_banner` | `True` | 启动时打印 Sa-Token 标志,`.print_banner(False)` 关闭 |
316
317
 
317
318
  Token 读取顺序全局唯一:**Header → Authorization 兜底 → Cookie → Query**。各框架禁止自己再解析一遍。
318
319
 
@@ -504,8 +505,8 @@ class MyappConfig(AppConfig):
504
505
  from sa_token import SaToken
505
506
  from sa_token.storage import MemoryStorage, RedisStorage
506
507
 
507
- SaToken.builder().storage(MemoryStorage()).timeout(86400).print_banner(False).build()
508
- SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).print_banner(False).build()
508
+ SaToken.builder().storage(MemoryStorage()).timeout(86400).build()
509
+ SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).build()
509
510
  ```
510
511
 
511
512
  ### 2. 注册中间件
@@ -1,16 +1,17 @@
1
1
  sa_token/__init__.py,sha256=pv7sfIXDmQMmRJuNRuXDb_q_bjVFnUrDw1kxk7gkhms,2165
2
- sa_token/config.py,sha256=CkRuH7mtBlnyp9DrbVZvbt1ESBjWbBKJndIjH2hbG6I,4540
2
+ sa_token/banner.py,sha256=vNNoPd2Q9ynT3FJvxOifAWFUI9f8QmCyOw5MTuzsCzE,2240
3
+ sa_token/config.py,sha256=vwwhUzLCMuw8RP9NPsalWy9HAGtR4VGsUhLCvCvlrWk,4841
3
4
  sa_token/context.py,sha256=cwI320c07fhHFkNawlhjOLQbhMkWm5jeb69vD3qVxuw,2002
4
5
  sa_token/exception.py,sha256=4KOgvZOx6nRuLB482Ehm5l841SWxRsiw_T-mbkR6Z14,3997
5
6
  sa_token/listener.py,sha256=pNLry8yllu35fTw9CnjmgaYLHuI8iyzS_d7NhXxoWm8,3129
6
- sa_token/manager.py,sha256=eSERfj8w2iIMCWLDlGKFsQPlZOyggfMqDNwfZLcSMI0,8287
7
+ sa_token/manager.py,sha256=XZkZJergXRs63r7VzzbxI9BfyOggmQ-ys3nvOh8kZ-0,7808
7
8
  sa_token/model.py,sha256=2Twvy2XXkTBuab1Mg4SSQfXRDXSIKaQ6xnu8NUtMN0w,4374
8
9
  sa_token/permission.py,sha256=B_KKGaANfPtFSdGlNrp0bk2s_ZVbeHkYvLzoOlwQvkk,2487
9
10
  sa_token/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  sa_token/session.py,sha256=9DkqLcPxppiqbVVwf2ybjSDv--4okWs__cPYbfsWGW0,2696
11
12
  sa_token/stp_interface.py,sha256=dxjPRj5jvuLLiGgh-o0EXhqouL42q59xob6e5IWhCBE,616
12
- sa_token/stp_logic.py,sha256=mWp9g_hAV8B3LQisGQD0RcYsuJe3N-dY7fTzL0E2k_Y,35627
13
- sa_token/stp_util.py,sha256=3G2jkDAulUsymQWxwf3AjrfcgUClt7oQmz8bNQ0iwgo,12685
13
+ sa_token/stp_logic.py,sha256=VvJG6wEZkTbpCW-Dm4SQkXmJ69tkU2ihyxRmUrBYWSc,35826
14
+ sa_token/stp_util.py,sha256=LRP_2csZybI-YxnuqQY-iCUDROwy51PtP9pV2hIu_RI,12747
14
15
  sa_token/sync.py,sha256=8ac7w2k0ER58jblXXV2ZaKk30qBiuwooPjniTjvWcjg,8890
15
16
  sa_token/token_io.py,sha256=rWqKJMGVZ-DCu0kjLMMZoaq2pR-rJql-96qkfyjkHuw,2174
16
17
  sa_token/adapter/__init__.py,sha256=NamL0PvdUaLMJgEs1Yo9MXeVkmFRUudhw9FtlELyEOc,638
@@ -18,18 +19,18 @@ sa_token/adapter/http.py,sha256=1pOdrT-1RcurejFYHj4MoNCD4Yr6UjcBiV5JdVeOwAc,1998
18
19
  sa_token/adapter/path.py,sha256=ZJpfY7SKVkDb5Pz9bdlUwsiJHyfP5FswIe80qbtApsU,5231
19
20
  sa_token/adapter/pipeline.py,sha256=0Eq_W6pOM4D6yV3LMomeA-XnrGB-GXSzbzn9k-7tyGI,2837
20
21
  sa_token/integration/__init__.py,sha256=lc4Bi1kdQnsN3McdcNPHptuOg8MY5InYCL7ZkugJk7I,380
21
- sa_token/integration/django.py,sha256=7E0ibwfGK9pt_fHTxnVQk5TcFNqNCtl339HQJhDF_PE,4178
22
+ sa_token/integration/django.py,sha256=IWqq7ytKytj3mC_waMe0ww1fgXatyKZh4NwLeL4FeMs,4255
22
23
  sa_token/integration/fastapi.py,sha256=oHNC_ErZikkbRWDXcBnWvR2gL9vZlNIKHcyUbG2FAyQ,9988
23
24
  sa_token/integration/fastapi_oauth2.py,sha256=2pZPgYtZiYBS7Wne7VRqlcGfjEz5jm882xBlOYIXgH8,5109
24
- sa_token/integration/flask.py,sha256=_PxsKbyTCqm22uy1EOSIhoKPlGvbY5SpN0l2JCazKZw,6351
25
- sa_token/integration/starlette.py,sha256=XIcKDhsIcjmjDDrUYegwhDYBZiK5LA5vjaP8XJ7akFA,7464
25
+ sa_token/integration/flask.py,sha256=rFkFvms5wvJWm0KsrFl8upCtUPxKJQCucxeMU7dNvdg,6526
26
+ sa_token/integration/starlette.py,sha256=bMZk2a6yBqqhayUSv9XcTuJ8SXp78oO5b0OgOt3nmBs,7541
26
27
  sa_token/oauth2/__init__.py,sha256=J4tV401Y7hcLiyKUhypxrOoaZwgx-jMV_tee1QwyCsM,487
27
28
  sa_token/oauth2/model.py,sha256=OWQCiNWKro4elFwgEeFz7iviW7IqHD4Dhw7OQ32rvi8,3764
28
29
  sa_token/oauth2/server.py,sha256=NSzjwFU1ukO1jK-gUAdxkJZTID4-rUO0p7ZzAL_AY6Q,13884
29
30
  sa_token/online/__init__.py,sha256=7rpfFYPvnSMhoVBLs6TCmwwpFGcjqP7BgszBGj3NSYg,11218
30
31
  sa_token/security/__init__.py,sha256=v8W2uxXwRD-F_GteZhl_v4NFQS4QkObRiIo6kl4ctzg,369
31
32
  sa_token/security/nonce.py,sha256=wc3fHjM_-lgjK-jrkFkfW4Zc4RkwZrgs5ceBp6Ce9ZQ,3344
32
- sa_token/security/refresh.py,sha256=C6cqFe0KHmMvweaJo9gkuodic48tZfZdP6vUy_HihPs,11430
33
+ sa_token/security/refresh.py,sha256=A5fmJG_SzrwHU77KX6fGBAcCIJmm2xg_631I4hLl8g8,12096
33
34
  sa_token/security/temp_token.py,sha256=Jgd9-Pk_Q5NVGesN5g41PhxfTRg1i4yr2Z6Z2cqM7M0,4454
34
35
  sa_token/sso/__init__.py,sha256=GYpX1oIGKMjTggUnHQCM9s-n3fN7NasdmCRZfnCCICU,7830
35
36
  sa_token/storage/__init__.py,sha256=7yv-26bmOT1cFDtsIYI1KmiPXkng7-3pe50uPzOTcXI,633
@@ -40,7 +41,7 @@ sa_token/strategy/__init__.py,sha256=j3DzIIMPqHF3L6_6VxxQAHtwEH9AX7GPv1ON-dyp67c
40
41
  sa_token/strategy/base.py,sha256=lNGwe-cW7gEnE6xn7_9gsCSkT4u-Ty5SZElqsoUMpck,634
41
42
  sa_token/strategy/builtin.py,sha256=2tYH32UXMbUu-Uup_yMouNSYNjO6XMXPNpnjZOGNMC0,2722
42
43
  sa_token/strategy/jwt.py,sha256=qZo2DoBG4Cq_tz5swtgasYiQACARzk-_ri-nO9_kWq8,2360
43
- sa_token_python_core-0.1.2.dist-info/METADATA,sha256=8euj85fNOos4pVUePjn8ROcRWdpkoXNvk6oJipKZOeY,26797
44
- sa_token_python_core-0.1.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
45
- sa_token_python_core-0.1.2.dist-info/licenses/LICENSE,sha256=KglpXsmvkzfIiEB2FvZnD5SuCyEtV1px4b4nTzQFHG8,10860
46
- sa_token_python_core-0.1.2.dist-info/RECORD,,
44
+ sa_token_python_core-0.1.4.dist-info/METADATA,sha256=k5dSP9KU6tzgLZjAcDtt2X9hp5WXmZ3sVj-kfuUULu4,26854
45
+ sa_token_python_core-0.1.4.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
46
+ sa_token_python_core-0.1.4.dist-info/licenses/LICENSE,sha256=KglpXsmvkzfIiEB2FvZnD5SuCyEtV1px4b4nTzQFHG8,10860
47
+ sa_token_python_core-0.1.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any