FastAPI JWT和hash加密
JWT 简介
JWT 即JSON 网络令牌(JSON Web Tokens)。
JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 字符串没有加密,任何人都能用它恢复原始信息。但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。
令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。
安装 PyJWT
安装 PyJWT,在 Python 中生成和校验 JWT 令牌:
pip install pyjwt -i https://mirrors.aliyun.com/pypi/simple/
安装这个JWT Python工具包也行 python-jose
在Python中生成和校验JWT令牌,Python-jose 支持PyJWT的所有功能,还支持与其他工具集成时可能会用到的一些其他功能。
pip install python-jose[cryptography] -i https://mirrors.aliyun.com/pypi/simple/
哈希密码
安装 passlib
Passlib 是处理密码哈希的 Python 包。它支持很多安全哈希算法及配套工具。本教程推荐的算法是 Bcrypt。因此,请先安装附带 Bcrypt 的 PassLib:
pip install passlib[bcrypt] -i https://mirrors.aliyun.com/pypi/simple/
解决版本的问题:
pip install passlib --upgrade -i https://mirrors.aliyun.com/pypi/simple/pip install bcrypt==3.2.0 -i https://mirrors.aliyun.com/pypi/simple/ # 一个已知稳定的版本
密码加密验证工具 encrypt_hash.py
from passlib.context import CryptContextclass Encrypt:"""哈希加密"""# 创建用于密码哈希和身份校验的 PassLib 上下文# schemes=["bcrypt"] bcrypt加密算法# deprecated="auto" 自动选择密码加密方案pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")@classmethoddef get_password_hash(cls, password)-> str:"""获取哈希密码:param password: 明文密码:return:"""return cls.pwd_context.hash(password)@classmethoddef verify_password_hash(cls, password, password_hash) -> bool:"""校验密码:param password: 明文密码:param password_hash: 哈希密码:return:"""return cls.pwd_context.verify(password, password_hash)if __name__ == '__main__':# $2b$12$.gkr.w01NbuufBn5ZZLJnuSmJo6A.CDZ3xLLWgJAkEhsAvfx6kTkm#print(Encrypt.get_password_hash("123"))print(Encrypt.verify_password_hash("123", "$2b$12$.gkr.w01NbuufBn5ZZLJnuSmJo6A.CDZ3xLLWgJAkEhsAvfx6kTkm"))
封装JWT工具
jwt.py文件,生成JWT Token
import time
from typing import Union, Anyfrom jose import jwtclass JWT:"""生成 JWT Token"""# JWT Token令牌有效时长ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 单位:分钟# 加密算法ALGORITHM = "HS256"# 密钥(加密盐)JWT_SECRET_KEY = "98uu7h66te4ss12xbx8zoo9pq77sdf53f0"@classmethoddef create_access_token(cls, subject: Union[str, Any], expires_delta: int = None )-> str:"""根据用户信息创建 JWT Token令牌:param subject: 用户信息,一般为 user_id:param expires_delta: 过期时长 单位: 秒 默认值None:return: token字符串"""# 有效时长if expires_delta:# int(time.time())当前时间,获取整数时间戳(秒级)expires_delta = int(time.time()) + expires_deltaelse:# 系统默认的过期时长 (秒)expires_delta = int(time.time()) + cls.ACCESS_TOKEN_EXPIRE_MINUTES * 60claims = {"exp": expires_delta,"sub": str(subject),}# 生成 JWT Tokenreturn jwt.encode(claims, cls.JWT_SECRET_KEY, algorithm=cls.ALGORITHM)if __name__ == '__main__':print(JWT.create_access_token("0001"))
校验JWT Token的中间件
access_token_middleware.py
import timefrom jose import jwt
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponsefrom utils.jwt import JWTclass AccessTokenMiddleware(BaseHTTPMiddleware):"""校验 JWT Token的中间件"""# 不需要访问令牌的URLno_access_token_urls = ["/docs","/user/register","/openapi.json",]async def dispatch(self, request, call_next):# 请求路径path = request.url.path# 如果请求路径需要Token令牌,则进行Token验证if path not in self.no_access_token_urls:# 从Header请求头中获取tokenauthorization = request.headers.get("Authorization")# 如果未获取到Token令牌,则抛出异常if authorization is None:# OAuth2规范,如果认证失败,请求头中返回 "WWW-Authenticate"return no_access_token_response()# 进行Token令牌校验access_token = authorization.split(' ')[1]try:subject: dict = jwt.decode(token=access_token,key=JWT.JWT_SECRET_KEY,algorithms=["HS256"],)# 获取用户IDuser_id = subject.get("sub").split(":")[1]# 缺失用户信息if user_id is None:return lack_user_response()# 判断令牌是否过期expires_delta = subject.get("exp").split(":")[1]if expires_delta is None:return lack_user_response()# 如果令牌过期,则重新登录elif expires_delta <= int(time.time()):return expire_token_response()# 将用户ID存放到Request对象的state中request.state.user_id = user_idexcept Exception as e:print(e)return token_exception_response()# 放行,继续向下执行return await call_next(request)def no_access_token_response():return JSONResponse(status_code=401,content={"code": 111111, "message": "未获取到访问令牌", "data": None},headers={"WWW-Authenticate": "Bearer"},)def lack_user_response():return JSONResponse(status_code=401,content={"code": 222222, "message": "缺失用户信息", "data": None},headers={"WWW-Authenticate": "Bearer"},)def token_exception_response():return JSONResponse(status_code=401,content={"code": 333333, "message": "访问令牌异常", "data": None},headers={"WWW-Authenticate": "Bearer"},)def expire_token_response():return JSONResponse(status_code=401,content={"code": 444444, "message": "令牌过期,请重新登录", "data": None},headers={"WWW-Authenticate": "Bearer"},)