feat: embed permission claims in JWT and add captcha verification
- Add GroupClaims model for JWT permission snapshots - Add JWTPayload model for typed JWT decoding - Refactor auth middleware: jwt_required (no DB) -> admin_required (no DB) -> auth_required (DB) - Add UserBanStore for instant ban enforcement via Redis + memory fallback - Fix status check bug: StrEnum is always truthy, use explicit != ACTIVE - Shorten access_token expiry from 3h to 1h - Add CaptchaScene enum and verify_captcha_if_needed service - Add require_captcha dependency injection factory - Add CLA document and new default settings - Update all tests for new JWT API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import jwt
|
||||
@@ -6,6 +7,9 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from sqlmodels import AccessTokenBase, RefreshTokenBase, TokenResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlmodels.group import GroupClaims
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
scheme_name='获取 JWT Bearer 令牌',
|
||||
description='用于获取 JWT Bearer 令牌,需要以表单的形式提交',
|
||||
@@ -59,7 +63,7 @@ def build_token_payload(
|
||||
elif is_refresh:
|
||||
expire = datetime.now(timezone.utc) + timedelta(days=30)
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(hours=3)
|
||||
expire = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
to_encode.update({
|
||||
"iat": int(datetime.now(timezone.utc).timestamp()),
|
||||
"exp": int(expire.timestamp())
|
||||
@@ -71,33 +75,36 @@ def build_token_payload(
|
||||
def create_access_token(
|
||||
sub: UUID,
|
||||
jti: UUID,
|
||||
*,
|
||||
status: str,
|
||||
group: "GroupClaims",
|
||||
expires_delta: timedelta | None = None,
|
||||
algorithm: str = "HS256",
|
||||
**kwargs
|
||||
) -> AccessTokenBase:
|
||||
"""
|
||||
生成访问令牌,默认有效期 3 小时。
|
||||
生成访问令牌,默认有效期 1 小时。
|
||||
|
||||
:param sub: 令牌的主题,通常是用户 ID。
|
||||
:param jti: 令牌的唯一标识符,通常是一个 UUID。
|
||||
:param expires_delta: 过期时间, 缺省时为 3 小时。
|
||||
:param status: 用户状态字符串。
|
||||
:param group: 用户组权限快照。
|
||||
:param expires_delta: 过期时间, 缺省时为 1 小时。
|
||||
:param algorithm: JWT 密钥强度,缺省时为 HS256
|
||||
:param kwargs: 需要放进 JWT Payload 的字段。
|
||||
|
||||
:return: 包含密钥本身和过期时间的 `AccessTokenBase`
|
||||
"""
|
||||
|
||||
data = {"sub": str(sub), "jti": str(jti)}
|
||||
|
||||
# 将额外的字段添加到 Payload 中
|
||||
for key, value in kwargs.items():
|
||||
data[key] = value
|
||||
data = {
|
||||
"sub": str(sub),
|
||||
"jti": str(jti),
|
||||
"status": status,
|
||||
"group": group.model_dump(mode="json"),
|
||||
}
|
||||
|
||||
access_token, expire_at = build_token_payload(
|
||||
data,
|
||||
False,
|
||||
algorithm,
|
||||
expires_delta
|
||||
data,
|
||||
False,
|
||||
algorithm,
|
||||
expires_delta,
|
||||
)
|
||||
return AccessTokenBase(
|
||||
access_token=access_token,
|
||||
|
||||
Reference in New Issue
Block a user