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/exception.py ADDED
@@ -0,0 +1,143 @@
1
+ """sa-token 异常体系。
2
+
3
+ 核心层只抛这些异常,框架适配层负责把它们翻译成各自的 HTTP 响应,
4
+ 禁止在适配层重新发明错误码。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+ __all__ = [
12
+ "NotLoginType",
13
+ "SaTokenException",
14
+ "SaTokenNotInitializedException",
15
+ "NotLoginException",
16
+ "NotPermissionException",
17
+ "NotRoleException",
18
+ "DisableException",
19
+ "NotSafeException",
20
+ "SecurityException",
21
+ ]
22
+
23
+
24
+ class NotLoginType(str, Enum):
25
+ """未登录的细分原因,用于让调用方区分「没带 token」和「被踢下线」。"""
26
+
27
+ NOT_TOKEN = "NOT_TOKEN"
28
+ INVALID_TOKEN = "INVALID_TOKEN"
29
+ TOKEN_TIMEOUT = "TOKEN_TIMEOUT"
30
+ TOKEN_FREEZE = "TOKEN_FREEZE"
31
+ BE_REPLACED = "BE_REPLACED"
32
+ KICK_OUT = "KICK_OUT"
33
+
34
+
35
+ _NOT_LOGIN_MESSAGES: dict[NotLoginType, str] = {
36
+ NotLoginType.NOT_TOKEN: "未提供 token",
37
+ NotLoginType.INVALID_TOKEN: "token 无效",
38
+ NotLoginType.TOKEN_TIMEOUT: "token 已过期",
39
+ NotLoginType.TOKEN_FREEZE: "token 已被冻结(长时间未活跃)",
40
+ NotLoginType.BE_REPLACED: "token 已被顶下线",
41
+ NotLoginType.KICK_OUT: "token 已被踢下线",
42
+ }
43
+
44
+
45
+ class SaTokenException(Exception):
46
+ """所有 sa-token 异常的基类。"""
47
+
48
+ http_status = 500
49
+
50
+ def __init__(self, message: str, *, login_type: str = "login") -> None:
51
+ super().__init__(message)
52
+ self.message = message
53
+ self.login_type = login_type
54
+
55
+
56
+ class SaTokenNotInitializedException(SaTokenException):
57
+ """在调用 StpUtil 之前没有构建 Manager。"""
58
+
59
+ def __init__(self) -> None:
60
+ super().__init__(
61
+ "sa-token 尚未初始化,请先调用 SaToken.builder()...build() 或 set_manager()"
62
+ )
63
+
64
+
65
+ class NotLoginException(SaTokenException):
66
+ """未登录、token 失效、被踢、被顶、被冻结。"""
67
+
68
+ http_status = 401
69
+
70
+ def __init__(
71
+ self,
72
+ not_login_type: NotLoginType,
73
+ *,
74
+ login_type: str = "login",
75
+ token: str | None = None,
76
+ ) -> None:
77
+ super().__init__(_NOT_LOGIN_MESSAGES[not_login_type], login_type=login_type)
78
+ self.type = not_login_type
79
+ self.token = token
80
+
81
+
82
+ class NotPermissionException(SaTokenException):
83
+ """权限不足。"""
84
+
85
+ http_status = 403
86
+
87
+ def __init__(self, permission: str, *, login_type: str = "login") -> None:
88
+ super().__init__(f"缺少权限:{permission}", login_type=login_type)
89
+ self.permission = permission
90
+
91
+
92
+ class NotRoleException(SaTokenException):
93
+ """角色不足。"""
94
+
95
+ http_status = 403
96
+
97
+ def __init__(self, role: str, *, login_type: str = "login") -> None:
98
+ super().__init__(f"缺少角色:{role}", login_type=login_type)
99
+ self.role = role
100
+
101
+
102
+ class DisableException(SaTokenException):
103
+ """账号(或账号的某项服务)被封禁。"""
104
+
105
+ http_status = 403
106
+
107
+ def __init__(
108
+ self,
109
+ login_id: str,
110
+ service: str,
111
+ level: int,
112
+ remaining: int,
113
+ *,
114
+ login_type: str = "login",
115
+ ) -> None:
116
+ super().__init__(
117
+ f"账号 {login_id} 的服务 {service} 已被封禁(等级 {level},剩余 {remaining} 秒)",
118
+ login_type=login_type,
119
+ )
120
+ self.login_id = login_id
121
+ self.service = service
122
+ self.level = level
123
+ self.remaining = remaining
124
+
125
+
126
+ class NotSafeException(SaTokenException):
127
+ """二级认证未通过。"""
128
+
129
+ http_status = 403
130
+
131
+ def __init__(self, business: str, *, login_type: str = "login") -> None:
132
+ super().__init__(f"二级认证未通过:{business}", login_type=login_type)
133
+ self.business = business
134
+
135
+
136
+ class SecurityException(SaTokenException):
137
+ """Nonce、Refresh Token、Temp Token 等安全流程错误。"""
138
+
139
+ http_status = 400
140
+
141
+ def __init__(self, code: str, message: str, *, login_type: str = "login") -> None:
142
+ super().__init__(message, login_type=login_type)
143
+ self.code = code
@@ -0,0 +1,10 @@
1
+ """框架绑定。
2
+
3
+ 每个子模块都只做「Request → HttpContext、调用统一管道、异常翻译」三件事,
4
+ 因此各框架的行为天然一致。这些模块依赖对应的可选 extra,
5
+ 不在这里做顶层导入,避免只装了核心包的用户 import 失败。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = ["fastapi", "starlette", "flask", "django"]
@@ -0,0 +1,131 @@
1
+ """Django 适配(同步中间件 + 装饰器)。
2
+
3
+ Django 自带一套完整的 Session/User 体系,因此这里的定位是:
4
+ 给**纯 API 项目**(DRF / 无模板)提供与 FastAPI、Flask 一致的 Token 鉴权,
5
+ 而不是替换 ``django.contrib.auth``。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ from collections.abc import Callable
12
+ from typing import Any, TypeVar
13
+
14
+ from ..adapter.http import HttpContext
15
+ from ..adapter.path import PathAuthConfig
16
+ from ..adapter.pipeline import build_rule, resolve_token, run_auth_flow, run_path_auth
17
+ from ..exception import SaTokenException
18
+ from ..permission import MatchMode
19
+ from ..stp_util import get_manager
20
+ from ..sync import run_sync
21
+
22
+ __all__ = [
23
+ "DjangoHttpContext",
24
+ "SaTokenDjangoMiddleware",
25
+ "check_login",
26
+ "check_permission",
27
+ "check_role",
28
+ "to_json_response",
29
+ ]
30
+
31
+ _F = TypeVar("_F", bound=Callable[..., Any])
32
+
33
+ #: 由项目在 settings 里赋值,用于开启路径鉴权。
34
+ PATH_AUTH: PathAuthConfig | None = None
35
+
36
+
37
+ class DjangoHttpContext(HttpContext):
38
+ def __init__(self, request: Any) -> None:
39
+ self._request = request
40
+ self.state: dict[str, Any] = {}
41
+
42
+ def get_header(self, name: str) -> str | None:
43
+ return self._request.headers.get(name)
44
+
45
+ def get_cookie(self, name: str) -> str | None:
46
+ return self._request.COOKIES.get(name)
47
+
48
+ def get_query(self, name: str) -> str | None:
49
+ return self._request.GET.get(name)
50
+
51
+ def get_path(self) -> str:
52
+ return self._request.path
53
+
54
+ def get_method(self) -> str:
55
+ return self._request.method
56
+
57
+
58
+ def to_json_response(exc: SaTokenException) -> Any:
59
+ from django.http import JsonResponse
60
+
61
+ payload: dict[str, Any] = {
62
+ "code": exc.http_status,
63
+ "message": exc.message,
64
+ "error": type(exc).__name__,
65
+ }
66
+ detail_type = getattr(exc, "type", None)
67
+ if detail_type is not None:
68
+ payload["type"] = detail_type.value
69
+ return JsonResponse(payload, status=exc.http_status)
70
+
71
+
72
+ class SaTokenDjangoMiddleware:
73
+ """加入 ``MIDDLEWARE`` 即可。
74
+
75
+ 默认只解析 token 并挂到 ``request.sa_token`` / ``request.sa_login_id``;
76
+ 把 :data:`PATH_AUTH` 设为规则表后才执行强制鉴权。
77
+ """
78
+
79
+ def __init__(self, get_response: Callable[[Any], Any]) -> None:
80
+ self._get_response = get_response
81
+
82
+ def __call__(self, request: Any) -> Any:
83
+ ctx = DjangoHttpContext(request)
84
+ manager = get_manager()
85
+ try:
86
+ if PATH_AUTH is None:
87
+ resolve_token(ctx, manager)
88
+ else:
89
+ run_sync(run_path_auth(ctx, manager, PATH_AUTH))
90
+ except SaTokenException as exc:
91
+ return to_json_response(exc)
92
+
93
+ request.sa_token = ctx.state.get("stp_token")
94
+ request.sa_login_id = ctx.state.get("stp_login_id")
95
+ return self._get_response(request)
96
+
97
+ def process_exception(self, request: Any, exception: Exception) -> Any:
98
+ if isinstance(exception, SaTokenException):
99
+ return to_json_response(exception)
100
+ return None
101
+
102
+
103
+ def _guard(rule_factory: Callable[[], Any]) -> Callable[[_F], _F]:
104
+ def decorator(view: _F) -> _F:
105
+ @functools.wraps(view)
106
+ def wrapper(request: Any, *args: Any, **kwargs: Any) -> Any:
107
+ ctx = DjangoHttpContext(request)
108
+ try:
109
+ result = run_sync(run_auth_flow(ctx, get_manager(), rule_factory()))
110
+ except SaTokenException as exc:
111
+ return to_json_response(exc)
112
+ request.sa_login_id = result.login_id
113
+ request.sa_token = result.token
114
+ return view(request, *args, **kwargs)
115
+
116
+ return wrapper # type: ignore[return-value]
117
+
118
+ return decorator
119
+
120
+
121
+ def check_login(view: _F) -> _F:
122
+ """标准注解:``@check_login``。"""
123
+ return _guard(build_rule)(view)
124
+
125
+
126
+ def check_permission(*permissions: str, mode: MatchMode = "OR") -> Callable[[_F], _F]:
127
+ return _guard(lambda: build_rule(permissions=list(permissions), mode=mode))
128
+
129
+
130
+ def check_role(*roles: str, mode: MatchMode = "OR") -> Callable[[_F], _F]:
131
+ return _guard(lambda: build_rule(roles=list(roles), mode=mode))
@@ -0,0 +1,315 @@
1
+ """FastAPI 适配。
2
+
3
+ 各框架的标准鉴权写法都是注解:``@sa.check_login`` / ``@sa.check_permission``。
4
+ :class:`SaTokenFastAPI` 提供这一套,与 Flask、Django 同一套 ``StpLogic``。
5
+
6
+ FastAPI 作为主要适配框架,额外提供 ``Depends`` / ``Annotated``(``LoginId``、
7
+ ``BearerLoginId`` 等),方便按 FastAPI 习惯写依赖注入。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import inspect
14
+ from collections.abc import Callable
15
+ from typing import Annotated, Any, TypeVar
16
+
17
+ try:
18
+ from fastapi import Depends, FastAPI, Request, Response, WebSocket
19
+ from fastapi.security import ( # pyright: ignore[reportMissingImports]
20
+ HTTPAuthorizationCredentials,
21
+ HTTPBearer,
22
+ )
23
+ except ImportError as exc: # pragma: no cover - 依赖缺失路径
24
+ raise ImportError(
25
+ '该模块需要 fastapi,请执行:pip install "sa-token-python-core[fastapi]"'
26
+ ) from exc
27
+
28
+ from ..adapter.path import PathAuthConfig
29
+ from ..adapter.pipeline import build_rule, run_auth_flow
30
+ from ..context import get_current_login_id, get_current_token
31
+ from ..exception import NotLoginException, NotLoginType
32
+ from ..permission import MatchMode
33
+ from ..stp_util import get_manager
34
+ from .starlette import (
35
+ SaTokenMiddleware,
36
+ StarletteHttpContext,
37
+ check_disable,
38
+ check_login,
39
+ check_permission,
40
+ check_role,
41
+ check_safe,
42
+ current_login_id,
43
+ current_login_id_or_none,
44
+ current_token,
45
+ install_exception_handlers,
46
+ sa_token_exception_handler,
47
+ )
48
+
49
+ _F = TypeVar("_F", bound=Callable[..., Any])
50
+
51
+ __all__ = [
52
+ "SaTokenFastAPI",
53
+ "SaTokenMiddleware",
54
+ "StarletteHttpContext",
55
+ "install_exception_handlers",
56
+ "sa_token_exception_handler",
57
+ "check_login",
58
+ "check_permission",
59
+ "check_role",
60
+ "check_disable",
61
+ "check_safe",
62
+ "current_login_id",
63
+ "current_login_id_or_none",
64
+ "current_token",
65
+ "LoginId",
66
+ "OptionalLoginId",
67
+ "TokenValue",
68
+ "BearerLoginId",
69
+ "set_token_cookie",
70
+ "delete_token_cookie",
71
+ "FastAPIWebSocketContext",
72
+ "authenticate_websocket",
73
+ ]
74
+
75
+
76
+ def _extract_request(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Request:
77
+ request = kwargs.get("request")
78
+ if isinstance(request, Request):
79
+ return request
80
+ for value in args:
81
+ if isinstance(value, Request):
82
+ return value
83
+ for value in kwargs.values():
84
+ if isinstance(value, Request):
85
+ return value
86
+ raise RuntimeError("未找到 Request")
87
+
88
+
89
+ def _endpoint_signature(view: Callable[..., Any]) -> inspect.Signature:
90
+ signature = inspect.signature(view)
91
+ if "request" in signature.parameters:
92
+ return signature
93
+ request_param = inspect.Parameter(
94
+ "request",
95
+ inspect.Parameter.KEYWORD_ONLY,
96
+ annotation=Request,
97
+ )
98
+ return signature.replace(parameters=[*signature.parameters.values(), request_param])
99
+
100
+
101
+ class SaTokenFastAPI:
102
+ """FastAPI 的标准注解鉴权,与 Flask / Django 同一套语义。
103
+
104
+ 用法::
105
+
106
+ app = FastAPI()
107
+ sa = SaTokenFastAPI(app)
108
+
109
+ @app.get("/user")
110
+ @sa.check_login
111
+ async def user_info():
112
+ return {"id": sa.login_id()}
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ app: FastAPI | None = None,
118
+ *,
119
+ path_auth: PathAuthConfig | None = None,
120
+ login_type: str = "login",
121
+ ) -> None:
122
+ self.path_auth = path_auth
123
+ self.login_type = login_type
124
+ if app is not None:
125
+ self.init_app(app)
126
+
127
+ def init_app(self, app: FastAPI) -> None:
128
+ app.add_middleware(
129
+ SaTokenMiddleware,
130
+ path_auth=self.path_auth,
131
+ login_type=self.login_type,
132
+ )
133
+ install_exception_handlers(app)
134
+
135
+ def login_id(self) -> str:
136
+ login_id = get_current_login_id()
137
+ if login_id:
138
+ return login_id
139
+ raise NotLoginException(NotLoginType.NOT_TOKEN, login_type=self.login_type)
140
+
141
+ def token(self) -> str | None:
142
+ return get_current_token()
143
+
144
+ def _guard(
145
+ self,
146
+ rule_factory: Callable[[], Any],
147
+ extra: Callable[[Any], Any] | None = None,
148
+ ) -> Callable[[_F], _F]:
149
+ def decorator(view: _F) -> _F:
150
+ had_request = "request" in inspect.signature(view).parameters
151
+
152
+ @functools.wraps(view)
153
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
154
+ request = _extract_request(args, kwargs)
155
+ ctx = StarletteHttpContext(request)
156
+ result = await run_auth_flow(
157
+ ctx,
158
+ get_manager(),
159
+ rule_factory(),
160
+ login_type=self.login_type,
161
+ )
162
+ request.state.sa_login_id = result.login_id
163
+ request.state.sa_token = result.token
164
+ if extra is not None:
165
+ extra_result = extra(result)
166
+ if inspect.isawaitable(extra_result):
167
+ await extra_result
168
+ call_kwargs = kwargs
169
+ call_args = args
170
+ if not had_request:
171
+ call_kwargs = dict(kwargs)
172
+ call_kwargs.pop("request", None)
173
+ call_args = tuple(item for item in args if not isinstance(item, Request))
174
+ outcome = view(*call_args, **call_kwargs)
175
+ if inspect.isawaitable(outcome):
176
+ return await outcome
177
+ return outcome
178
+
179
+ wrapper.__signature__ = _endpoint_signature(view)
180
+ return wrapper # type: ignore[return-value]
181
+
182
+ return decorator
183
+
184
+ @property
185
+ def check_login(self) -> Callable[[_F], _F]:
186
+ """标准注解:``@sa.check_login``。"""
187
+ return self._guard(build_rule)
188
+
189
+ def check_permission(self, *permissions: str, mode: MatchMode = "OR") -> Callable[[_F], _F]:
190
+ """标准注解:``@sa.check_permission("order:delete")``。"""
191
+ return self._guard(lambda: build_rule(permissions=list(permissions), mode=mode))
192
+
193
+ def check_role(self, *roles: str, mode: MatchMode = "OR") -> Callable[[_F], _F]:
194
+ return self._guard(lambda: build_rule(roles=list(roles), mode=mode))
195
+
196
+ def check_safe(self, business: str) -> Callable[[_F], _F]:
197
+ async def extra(result: Any) -> None:
198
+ await get_manager().stp(self.login_type).check_safe(result.token, business)
199
+
200
+ return self._guard(build_rule, extra=extra)
201
+
202
+ def check_disable(self, service: str = "login", level: int = 1) -> Callable[[_F], _F]:
203
+ async def extra(result: Any) -> None:
204
+ await get_manager().stp(self.login_type).check_disable(
205
+ result.login_id,
206
+ service=service,
207
+ level=level,
208
+ )
209
+
210
+ return self._guard(build_rule, extra=extra)
211
+
212
+
213
+ # FastAPI 惯用扩展:Depends / Annotated。
214
+
215
+ LoginId = Annotated[str, Depends(current_login_id)]
216
+ OptionalLoginId = Annotated[str | None, Depends(current_login_id_or_none)]
217
+ TokenValue = Annotated[str | None, Depends(current_token)]
218
+
219
+ _bearer_scheme = HTTPBearer(auto_error=False)
220
+
221
+
222
+ async def _bearer_login_id(
223
+ request: Request,
224
+ credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
225
+ ) -> str:
226
+ """Depends 扩展:把 Bearer 写进 OpenAPI,读取逻辑与注解鉴权相同。"""
227
+ del credentials
228
+ return await current_login_id(request)
229
+
230
+
231
+ BearerLoginId = Annotated[str, Depends(_bearer_login_id)]
232
+
233
+
234
+ def set_token_cookie(
235
+ response: Response,
236
+ token: str,
237
+ *,
238
+ max_age: int | None = None,
239
+ ) -> None:
240
+ """按全局 Cookie 配置把登录 token 写入响应。
241
+
242
+ 这是显式函数而不是自动拦截 ``StpUtil.login``:核心层没有 Response,
243
+ 自动写入会破坏框架无关边界。
244
+ """
245
+ from ..stp_util import get_manager
246
+
247
+ config = get_manager().config
248
+ if not config.is_write_cookie:
249
+ return
250
+ resolved_max_age = config.timeout if max_age is None else max_age
251
+ response.set_cookie(
252
+ key=config.token_name,
253
+ value=token,
254
+ max_age=None if resolved_max_age < 0 else resolved_max_age,
255
+ path=config.cookie_path,
256
+ domain=config.cookie_domain,
257
+ secure=config.cookie_secure,
258
+ httponly=config.cookie_http_only,
259
+ samesite=config.cookie_same_site,
260
+ )
261
+
262
+
263
+ def delete_token_cookie(response: Response) -> None:
264
+ """删除登录 Cookie,参数与写入时保持一致。"""
265
+ from ..stp_util import get_manager
266
+
267
+ config = get_manager().config
268
+ response.delete_cookie(
269
+ key=config.token_name,
270
+ path=config.cookie_path,
271
+ domain=config.cookie_domain,
272
+ secure=config.cookie_secure,
273
+ httponly=config.cookie_http_only,
274
+ samesite=config.cookie_same_site,
275
+ )
276
+
277
+
278
+ class FastAPIWebSocketContext:
279
+ """把 FastAPI / Starlette WebSocket 包装成核心 HttpContext。"""
280
+
281
+ def __init__(self, websocket: WebSocket) -> None:
282
+ self.websocket = websocket
283
+ self.state: dict[str, Any] = {}
284
+
285
+ def get_header(self, name: str) -> str | None:
286
+ return self.websocket.headers.get(name)
287
+
288
+ def get_cookie(self, name: str) -> str | None:
289
+ return self.websocket.cookies.get(name)
290
+
291
+ def get_query(self, name: str) -> str | None:
292
+ return self.websocket.query_params.get(name)
293
+
294
+ def get_path(self) -> str:
295
+ return self.websocket.url.path
296
+
297
+ def get_method(self) -> str:
298
+ return "WEBSOCKET"
299
+
300
+
301
+ async def authenticate_websocket(
302
+ websocket: WebSocket,
303
+ *,
304
+ login_type: str = "login",
305
+ ) -> tuple[str, str]:
306
+ """在 ``accept`` 之前校验 WebSocket,返回 ``(login_id, token)``。"""
307
+ from ..online import WebSocketAuthenticator
308
+ from ..stp_util import get_manager
309
+
310
+ context = FastAPIWebSocketContext(websocket)
311
+ login_id = await WebSocketAuthenticator(
312
+ get_manager(), login_type=login_type
313
+ ).authenticate(context)
314
+ token = context.state["stp_token"]
315
+ return login_id, token