fix: 后端 JWT 验证改为 HS256 方式提升认证可靠性

This commit is contained in:
qzl
2026-03-10 17:43:55 +08:00
parent 5d839192ab
commit 95d6927724
4 changed files with 177 additions and 310 deletions
+27 -30
View File
@@ -9,56 +9,53 @@ class TokenValidationError(Exception):
pass
class TokenVerifierUnavailableError(Exception):
pass
class JwtVerifier:
_expected_audience = "authenticated"
def __init__(
self,
jwks_url: str,
issuer: str,
audience: str,
apikey: str,
jwt_secret: str,
jwt_algorithm: str,
) -> None:
if jwt_algorithm != "HS256":
raise TokenValidationError("Unsupported JWT algorithm")
self._issuer: str = issuer
self._audience: str = audience
self._jwks_client: jwt.PyJWKClient = jwt.PyJWKClient(
jwks_url,
headers={
"apikey": apikey,
"Authorization": f"Bearer {apikey}",
},
)
self._jwt_secret: str = jwt_secret
self._jwt_algorithm: str = jwt_algorithm
def verify(self, token: str) -> dict[str, Any]:
try:
key = self._jwks_client.get_signing_key_from_jwt(token)
except jwt.PyJWKClientConnectionError as exc:
raise TokenVerifierUnavailableError("Unable to fetch JWKS") from exc
except jwt.PyJWKClientError as exc:
raise TokenValidationError("Unable to resolve signing key") from exc
try:
payload = jwt.decode(
token,
key.key,
algorithms=["RS256"],
audience=self._audience,
issuer=self._issuer,
options={"require": ["sub", "aud", "iss", "exp"]},
self._jwt_secret,
algorithms=[self._jwt_algorithm],
options={"require": ["sub", "exp", "aud"], "verify_aud": False},
)
except (
jwt.ExpiredSignatureError,
jwt.InvalidAudienceError,
jwt.InvalidIssuerError,
jwt.InvalidSignatureError,
jwt.InvalidAlgorithmError,
jwt.DecodeError,
jwt.PyJWTError,
) as exc:
raise TokenValidationError("Token validation failed") from exc
if not isinstance(payload, dict):
raise TokenValidationError("Token payload must be a JSON object")
token_audience = payload.get("aud")
if isinstance(token_audience, str):
audience_match = token_audience == self._expected_audience
elif isinstance(token_audience, list):
audience_match = self._expected_audience in token_audience
else:
audience_match = False
if not audience_match:
raise TokenValidationError("Token audience mismatch")
token_issuer = payload.get("iss")
if token_issuer is not None and token_issuer != self._issuer:
raise TokenValidationError("Token issuer mismatch")
return cast(dict[str, Any], payload)
+3 -5
View File
@@ -8,6 +8,7 @@ from pydantic import (
AnyHttpUrl,
BaseModel,
Field,
SecretStr,
computed_field,
field_validator,
model_validator,
@@ -126,9 +127,9 @@ class SupabaseSettings(BaseModel):
public_url: AnyHttpUrl
anon_key: str = "CHANGE_ME"
service_role_key: str = "CHANGE_ME"
jwt_audience: str = "authenticated"
jwt_secret: SecretStr | None = Field(default=None, exclude=True)
jwt_algorithm: Literal["HS256"] = "HS256"
jwt_issuer: str | None = None
jwks_url: str | None = None
@model_validator(mode="after")
def compute_defaults(self) -> "SupabaseSettings":
@@ -136,9 +137,6 @@ class SupabaseSettings(BaseModel):
if self.jwt_issuer is None:
self.jwt_issuer = f"{base}/auth/v1"
if self.jwks_url is None:
self.jwks_url = f"{self.jwt_issuer}/.well-known/jwks.json"
return self
@computed_field