sa-token-python-core 0.1.2__tar.gz → 0.1.4__tar.gz
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_python_core-0.1.2 → sa_token_python_core-0.1.4}/PKG-INFO +4 -3
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/README.md +3 -2
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/examples/native/quick_start.py +1 -1
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/pyproject.toml +1 -1
- sa_token_python_core-0.1.4/src/sa_token/banner.py +76 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/config.py +7 -1
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/django.py +2 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/flask.py +4 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/starlette.py +2 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/manager.py +6 -22
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/security/refresh.py +21 -5
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/stp_logic.py +3 -1
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/stp_util.py +3 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/conftest.py +9 -0
- sa_token_python_core-0.1.4/tests/test_banner.py +106 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_security.py +34 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/.github/workflows/ci.yml +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/.gitignore +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/LICENSE +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/examples/fastapi/main.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/examples/flask/main.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/adapter/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/adapter/http.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/adapter/path.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/adapter/pipeline.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/context.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/exception.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/fastapi.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/fastapi_oauth2.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/listener.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/model.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/oauth2/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/oauth2/model.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/oauth2/server.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/online/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/permission.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/py.typed +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/security/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/security/nonce.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/security/temp_token.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/session.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/sso/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/storage/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/storage/base.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/storage/memory.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/storage/redis.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/stp_interface.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/strategy/__init__.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/strategy/base.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/strategy/builtin.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/strategy/jwt.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/sync.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/token_io.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_adapter.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_auth.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_authorization.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_core_isolation.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_extensions.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_integration_fastapi.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_integration_fastapi_oauth2.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_integration_flask.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_optional_backends.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_permission.py +0 -0
- {sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_storage_and_token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sa-token-python-core
|
|
3
|
-
Version: 0.1.
|
|
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).
|
|
508
|
-
SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).
|
|
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. 注册中间件
|
|
@@ -263,6 +263,7 @@ SaToken.builder() \
|
|
|
263
263
|
| `is_write_cookie` | `False` | FastAPI 显式调用写 Cookie 助手时是否生效 |
|
|
264
264
|
| `storage_key_prefix` | `satoken:` | 所有存储键前缀 |
|
|
265
265
|
| `jwt_secret_key` | `None` | JWT 风格时必填 |
|
|
266
|
+
| `is_print_banner` | `True` | 启动时打印 Sa-Token 标志,`.print_banner(False)` 关闭 |
|
|
266
267
|
|
|
267
268
|
Token 读取顺序全局唯一:**Header → Authorization 兜底 → Cookie → Query**。各框架禁止自己再解析一遍。
|
|
268
269
|
|
|
@@ -454,8 +455,8 @@ class MyappConfig(AppConfig):
|
|
|
454
455
|
from sa_token import SaToken
|
|
455
456
|
from sa_token.storage import MemoryStorage, RedisStorage
|
|
456
457
|
|
|
457
|
-
SaToken.builder().storage(MemoryStorage()).timeout(86400).
|
|
458
|
-
SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).
|
|
458
|
+
SaToken.builder().storage(MemoryStorage()).timeout(86400).build()
|
|
459
|
+
SaToken.builder().storage(RedisStorage.from_url("redis://127.0.0.1:6379/0")).timeout(86400).build()
|
|
459
460
|
```
|
|
460
461
|
|
|
461
462
|
### 2. 注册中间件
|
|
@@ -24,7 +24,7 @@ def on_auth_event(data: EventData) -> None:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
async def main() -> None:
|
|
27
|
-
SaToken.builder().storage(MemoryStorage()).timeout(7200).
|
|
27
|
+
SaToken.builder().storage(MemoryStorage()).timeout(7200).on(
|
|
28
28
|
Event.ALL, on_auth_event
|
|
29
29
|
).build()
|
|
30
30
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sa-token-python-core"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "有状态 Token 认证与权限鉴权框架(轻量核心)。灵感来源于 Java 的 Sa-Token。常用依赖请安装 sa-token-python;Web 框架按 extras 安装。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -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)
|
|
@@ -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 / 在线用户)放自定义配置。"""
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/django.py
RENAMED
|
@@ -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
|
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/starlette.py
RENAMED
|
@@ -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,
|
|
@@ -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.
|
|
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
|
-
|
|
104
|
-
|
|
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(
|
|
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
|
-
|
|
249
|
-
|
|
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
|
|
|
@@ -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)
|
|
@@ -3,10 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from sa_token import SaToken, SaTokenManager
|
|
6
|
+
from sa_token.banner import reset_banner_state
|
|
6
7
|
from sa_token.storage import MemoryStorage
|
|
7
8
|
from sa_token.stp_util import clear_manager
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def _isolate_banner():
|
|
13
|
+
reset_banner_state()
|
|
14
|
+
yield
|
|
15
|
+
reset_banner_state()
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
@pytest.fixture
|
|
11
19
|
def build_manager():
|
|
12
20
|
"""返回一个构建 Manager 的工厂,测试之间互不干扰。"""
|
|
@@ -22,6 +30,7 @@ def build_manager():
|
|
|
22
30
|
|
|
23
31
|
yield _build
|
|
24
32
|
clear_manager()
|
|
33
|
+
reset_banner_state()
|
|
25
34
|
|
|
26
35
|
|
|
27
36
|
@pytest.fixture
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""启动横幅:默认打印一次,可通过配置关闭。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from sa_token import SaToken, SaTokenConfig, SaTokenManager, __version__, set_manager
|
|
8
|
+
from sa_token.banner import emit_configured_banner, reset_banner_state
|
|
9
|
+
from sa_token.storage import MemoryStorage
|
|
10
|
+
from sa_token.stp_util import clear_manager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_build_prints_banner_by_default(capsys) -> None:
|
|
14
|
+
SaToken.builder().storage(MemoryStorage()).as_global(False).build()
|
|
15
|
+
|
|
16
|
+
output = capsys.readouterr().out
|
|
17
|
+
assert "sa-token-python" in output
|
|
18
|
+
assert f"(v{__version__})" in output
|
|
19
|
+
assert "MemoryStorage" in output
|
|
20
|
+
assert r"/ ___/____ _" in output
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_print_banner_false_silences_output(capsys) -> None:
|
|
24
|
+
SaToken.builder().storage(MemoryStorage()).print_banner(False).as_global(False).build()
|
|
25
|
+
|
|
26
|
+
assert capsys.readouterr().out == ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_set_option_can_disable_banner(capsys) -> None:
|
|
30
|
+
SaToken.builder().storage(MemoryStorage()).set_option(is_print_banner=False).as_global(
|
|
31
|
+
False
|
|
32
|
+
).build()
|
|
33
|
+
|
|
34
|
+
assert capsys.readouterr().out == ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_banner_prints_only_once(capsys) -> None:
|
|
38
|
+
manager = SaToken.builder().storage(MemoryStorage()).as_global(False).build()
|
|
39
|
+
capsys.readouterr()
|
|
40
|
+
|
|
41
|
+
manager.print_banner()
|
|
42
|
+
assert "sa-token-python" in capsys.readouterr().out
|
|
43
|
+
|
|
44
|
+
set_manager(manager)
|
|
45
|
+
emit_configured_banner()
|
|
46
|
+
assert capsys.readouterr().out == ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_direct_manager_construction_prints(capsys) -> None:
|
|
50
|
+
SaTokenManager(SaTokenConfig(), MemoryStorage())
|
|
51
|
+
|
|
52
|
+
output = capsys.readouterr().out
|
|
53
|
+
assert "sa-token-python" in output
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_emit_configured_banner_without_manager_is_silent(capsys) -> None:
|
|
57
|
+
clear_manager()
|
|
58
|
+
emit_configured_banner()
|
|
59
|
+
assert capsys.readouterr().out == ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_reset_allows_reprint(capsys) -> None:
|
|
63
|
+
SaToken.builder().storage(MemoryStorage()).as_global(False).build()
|
|
64
|
+
assert "sa-token-python" in capsys.readouterr().out
|
|
65
|
+
|
|
66
|
+
reset_banner_state()
|
|
67
|
+
SaToken.builder().storage(MemoryStorage()).as_global(False).build()
|
|
68
|
+
assert "sa-token-python" in capsys.readouterr().out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_fastapi_startup_prints_banner(capsys) -> None:
|
|
72
|
+
pytest.importorskip("fastapi")
|
|
73
|
+
from fastapi import FastAPI
|
|
74
|
+
from fastapi.testclient import TestClient
|
|
75
|
+
|
|
76
|
+
from sa_token.integration.fastapi import SaTokenMiddleware
|
|
77
|
+
|
|
78
|
+
manager = SaToken.builder().storage(MemoryStorage()).print_banner(False).build()
|
|
79
|
+
manager.config.is_print_banner = True
|
|
80
|
+
reset_banner_state()
|
|
81
|
+
|
|
82
|
+
app = FastAPI()
|
|
83
|
+
app.add_middleware(SaTokenMiddleware)
|
|
84
|
+
with TestClient(app):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
output = capsys.readouterr().out
|
|
88
|
+
assert "sa-token-python" in output
|
|
89
|
+
assert "MemoryStorage" in output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_flask_init_prints_banner(capsys) -> None:
|
|
93
|
+
pytest.importorskip("flask")
|
|
94
|
+
from flask import Flask
|
|
95
|
+
|
|
96
|
+
from sa_token.integration.flask import SaTokenFlask
|
|
97
|
+
|
|
98
|
+
SaToken.builder().storage(MemoryStorage()).build()
|
|
99
|
+
capsys.readouterr()
|
|
100
|
+
reset_banner_state()
|
|
101
|
+
|
|
102
|
+
app = Flask(__name__)
|
|
103
|
+
SaTokenFlask(app)
|
|
104
|
+
|
|
105
|
+
output = capsys.readouterr().out
|
|
106
|
+
assert "sa-token-python" in output
|
|
@@ -112,3 +112,37 @@ async def test_kickout_revokes_refresh_family(stp, manager) -> None:
|
|
|
112
112
|
await stp.kickout("10001")
|
|
113
113
|
with pytest.raises(SecurityException):
|
|
114
114
|
await manager.refresh_tokens.refresh(pair.refresh_token)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def test_kickout_after_refresh_login_keeps_kick_out_reason(stp, manager) -> None:
|
|
118
|
+
pair = await stp.login_with_refresh("10001")
|
|
119
|
+
await stp.kickout("10001")
|
|
120
|
+
|
|
121
|
+
with pytest.raises(NotLoginException) as error:
|
|
122
|
+
await stp.check_login(pair.access_token)
|
|
123
|
+
assert error.value.type is NotLoginType.KICK_OUT
|
|
124
|
+
|
|
125
|
+
reason = await stp.get_offline_reason(pair.access_token)
|
|
126
|
+
assert reason is not None
|
|
127
|
+
assert reason["reason"] == "KICK_OUT"
|
|
128
|
+
|
|
129
|
+
with pytest.raises(SecurityException):
|
|
130
|
+
await manager.refresh_tokens.refresh(pair.refresh_token)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def test_replaced_after_refresh_login_keeps_replaced_reason(stp) -> None:
|
|
134
|
+
pair = await stp.login_with_refresh("10001")
|
|
135
|
+
await stp.replaced("10001")
|
|
136
|
+
|
|
137
|
+
with pytest.raises(NotLoginException) as error:
|
|
138
|
+
await stp.check_login(pair.access_token)
|
|
139
|
+
assert error.value.type is NotLoginType.BE_REPLACED
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def test_kickout_by_token_after_refresh_login_keeps_kick_out_reason(stp) -> None:
|
|
143
|
+
pair = await stp.login_with_refresh("10001")
|
|
144
|
+
await stp.kickout_by_token(pair.access_token)
|
|
145
|
+
|
|
146
|
+
with pytest.raises(NotLoginException) as error:
|
|
147
|
+
await stp.check_login(pair.access_token)
|
|
148
|
+
assert error.value.type is NotLoginType.KICK_OUT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/__init__.py
RENAMED
|
File without changes
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/fastapi.py
RENAMED
|
File without changes
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/integration/fastapi_oauth2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/src/sa_token/security/temp_token.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sa_token_python_core-0.1.2 → sa_token_python_core-0.1.4}/tests/test_integration_fastapi_oauth2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|