feat(gateway): 接入42和72并为42/52/72启用可刷新令牌
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""Expiring client credentials for the protected gateway interfaces."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Security
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from gateway.config import get_config
|
||||
|
||||
ACCESS_TTL = 15 * 60
|
||||
REFRESH_TTL = 7 * 24 * 60 * 60
|
||||
_USERNAME = "yonghe"
|
||||
_PASSWORD = "yonghe1999"
|
||||
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["客户端鉴权"])
|
||||
_bearer = HTTPBearer(auto_error=False, scheme_name="ClientBearer")
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str = Field(max_length=128)
|
||||
password: str = Field(max_length=256)
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str = Field(max_length=1024)
|
||||
|
||||
|
||||
def _response(code, message, *, status=200, data=None, challenge=False):
|
||||
headers = {"Cache-Control": "no-store"}
|
||||
if challenge:
|
||||
headers["WWW-Authenticate"] = "Bearer"
|
||||
return JSONResponse(status_code=status, headers=headers, content={
|
||||
"code": code, "message": message, "request_id": f"gw-{uuid4().hex[:8]}", "data": data,
|
||||
})
|
||||
|
||||
|
||||
def _secret():
|
||||
value = os.getenv("HAIR_API_TOKEN_SECRET")
|
||||
if value is None:
|
||||
try:
|
||||
value = get_config().get("api_token_secret")
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if not isinstance(value, str) or len(value.encode("utf-8")) < 32:
|
||||
return None
|
||||
return value.encode("utf-8")
|
||||
|
||||
|
||||
def _encode(raw):
|
||||
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def _decode(value):
|
||||
return base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True)
|
||||
|
||||
|
||||
def _issue(secret, kind, ttl):
|
||||
payload = _encode(json.dumps({
|
||||
"sub": _USERNAME,
|
||||
"kind": kind,
|
||||
"exp": int(time.time()) + ttl,
|
||||
"nonce": secrets.token_urlsafe(16),
|
||||
}, separators=(",", ":")).encode())
|
||||
signature = hmac.digest(secret, payload.encode("ascii"), hashlib.sha256)
|
||||
return payload + "." + _encode(signature)
|
||||
|
||||
|
||||
def _valid(token, secret, kind):
|
||||
if not token or len(token) > 1024:
|
||||
return False
|
||||
try:
|
||||
payload, signature = token.split(".")
|
||||
expected = _encode(hmac.digest(secret, payload.encode("ascii"), hashlib.sha256))
|
||||
if not hmac.compare_digest(signature.encode("ascii"), expected.encode("ascii")):
|
||||
return False
|
||||
claims = json.loads(_decode(payload))
|
||||
return (
|
||||
isinstance(claims, dict)
|
||||
and claims.get("sub") == _USERNAME
|
||||
and claims.get("kind") == kind
|
||||
and type(claims.get("exp")) is int
|
||||
and time.time() < claims["exp"]
|
||||
)
|
||||
except (ValueError, UnicodeError, binascii.Error):
|
||||
return False
|
||||
|
||||
|
||||
def _unconfigured():
|
||||
return _response(1007, "客户端令牌服务未配置", status=503)
|
||||
|
||||
|
||||
def _unauthorized(message):
|
||||
return _response(1009, message, status=401, challenge=True)
|
||||
|
||||
|
||||
def _access_data(secret):
|
||||
return {
|
||||
"access_token": _issue(secret, "access", ACCESS_TTL),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TTL,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/login", summary="用户名和密码换取访问及刷新令牌")
|
||||
async def login(body: LoginRequest):
|
||||
username_ok = hmac.compare_digest(body.username.encode("utf-8"), _USERNAME.encode("utf-8"))
|
||||
password_ok = hmac.compare_digest(body.password.encode("utf-8"), _PASSWORD.encode("utf-8"))
|
||||
if not (username_ok and password_ok):
|
||||
return _unauthorized("用户名或密码错误")
|
||||
secret = _secret()
|
||||
if secret is None:
|
||||
return _unconfigured()
|
||||
data = _access_data(secret)
|
||||
data.update(refresh_token=_issue(secret, "refresh", REFRESH_TTL), refresh_expires_in=REFRESH_TTL)
|
||||
return _response(0, "success", data=data)
|
||||
|
||||
|
||||
@router.post("/refresh", summary="刷新访问令牌")
|
||||
async def refresh(body: RefreshRequest):
|
||||
secret = _secret()
|
||||
if secret is None:
|
||||
return _unconfigured()
|
||||
if not _valid(body.refresh_token, secret, "refresh"):
|
||||
return _unauthorized("刷新令牌无效或已过期,请重新登录")
|
||||
return _response(0, "success", data=_access_data(secret))
|
||||
|
||||
|
||||
async def client_auth_error(credentials: HTTPAuthorizationCredentials | None = Security(_bearer)):
|
||||
"""Return an error response for protected handlers before local work or dispatch."""
|
||||
if credentials is None:
|
||||
return _unauthorized("请提供 Authorization: Bearer 访问令牌")
|
||||
secret = _secret()
|
||||
if secret is None:
|
||||
return _unconfigured()
|
||||
if not _valid(credentials.credentials, secret, "access"):
|
||||
return _unauthorized("访问令牌无效或已过期")
|
||||
return None
|
||||
Reference in New Issue
Block a user