JWT 即JSON 网络令牌(JSON Web Tokens)。
JWT(JSON Web Token) 是一种用于在身份提供者和服务提供者之间传递身份验证和授权数据的开放标准。JWT是一个JSON对象,其中包含了被签名的声明。这些声明可以是身份验证的声明、授权的声明等。JWT可以使用数字签名进行签名,以确保它不被篡改。
JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 字符串没有加密,任何人都能用它恢复原始信息。
jwt官网及在线解码
但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。
例如:使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。
JWT的格式为:
xxx.xxx.xxx
JWT (JSON Web Token) 是一种用于在双方之间传递身份验证信息的标准。它是一个 JSON 对象,包含了被签名的声明。这些声明可以是身份验证的信息,比如用户名和密码,也可以是其他与此有关的信息,比如用户的权限。
Bearer JWT 是 JWT 的一种使用方式。在这种方式中,JWT 被用作认证授权的令牌,并通过 HTTP 请求的 Authorization
头部进行发送。这个头部的值是 Bearer空格
加上 JWT。
所以 JWT 是一种身份验证和授权的标准, Bearer JWT 则是 JWT在HTTP请求中的一种传递方式。
openssl rand -hex 32
是一个命令行命令,它使用 OpenSSL 库生成 32 个字符的十六进制随机字符串。
所以这个命令就是生成32位十六进制随机字符串。这种随机字符串常用来做盐值,可以用来加密密码,来防止密码被破解。
例如:
$ openssl rand -hex 32
> 6b48014c061a82bb3c6fd4812777552cf41a79c38149e31516b818a73d50ee51
python-jose
python-jose包用于在Python中 生成和校验 JWT 令牌
python-jose官网
python-jose需要安装配套的加密后端,例如下面我们使用的后端是pyca/cryptography
$ pip3 install "python-jose[cryptography]"
哈希是指把特定内容(本例中为密码)转换为乱码形式的字节序列(其实就是字符串)。
每次传入完全相同的内容时(比如,完全相同的密码),返回的都是完全相同的乱码。
但这个乱码无法转换回传入的密码。
原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。
这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大)。
bcrypt是一种密码哈希算法。它通过对用户的密码进行加密来保护它们。
bcrypt使用一种称为"慢哈希"的技术来提高安全性。在这种技术中,算法会在计算哈希值时进行大量迭代,从而使得暴力破解变得更加困难。
bcrypt 也支持一种称为 “盐” 的概念。盐是一串随机字符串,它被附加到密码上,然后一起计算哈希值。这样即使两个用户使用了相同的密码,它们的哈希值也会不同。
bcrypt 是被广泛接受的密码哈希算法,因为它能够高效地防止暴力破解,并且它的实现方式可以防止在多种并行计算环境中的高速破解。
在Python中可以使用bcrypt库来使用bcrypt算法。
import bcrypt
password = b"supersecretpassword"
# Hash a password for the first time, with a randomly-generated salt
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
# Check that an unencrypted password matches one that has
# previously been hashed
if bcrypt.checkpw(password, hashed):
print("It Matches!")
else:
print("It Does not Match :(")
passlib
Passlib 是处理密码哈希的 Python 包。
它支持很多安全哈希算法及配套工具。
本教程推荐的算法是 Bcrypt。因此,请先安装附带 Bcrypt 的 PassLib:
$ pip3 install passlib[bcrypt]
passlib
甚至可以读取 Django、Flask 的安全插件等工具创建的密码。
例如,把 Django 应用的数据共享给 FastAPI 应用的数据库。或利用同一个数据库,可以逐步把应用从 Django 迁移到 FastAPI。
并且,用户可以同时从 Django 应用或 FastAPI 应用登录。
"""
passlib.context.CryptContext 类可以用来配置和管理密码哈希算法。
这个实例的意思是:使用 "bcrypt" 算法对密码进行哈希,并使用 "auto" 模式对过时的算法进行处理。
"bcrypt" 是一种常用的密码哈希算法,它可以防止密码被暴力破解。
"auto" 模式表示自动处理过时的算法,例如在验证时优先使用最新的算法,如果失败则尝试使用旧的算法。
这个配置可以确保密码安全性,并且对过时算法进行兼容性处理。
Passlib 是一个强大的密码管理库,提供了一组密码哈希,验证和生成密码的工具,并且支持多种常用的密码哈希算法。
"""
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
"""验证输入的密码的hash密码与数据库中记录的hash密码是否是一样的
:param plain_password: 用户输入的密码
:param hashed_password: hash密码
:return:
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
"""把密码转为hash密码
"""
return pwd_context.hash(password)
if __name__ == '__main__':
password_hash = get_password_hash("Abc123.")
print(password_hash) # 注意:每次运行打印的值都是不同的
print(verify_password("Abc123.", password_hash)) # True
print(verify_password("abc123.", password_hash)) # False
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Union
from passlib.context import CryptContext
from pydantic import BaseModel
# 要获取如下所示的字符串,请运行:openssl rand -hex 32
# SECRET_KEY用于JWT令牌签名的随机密钥
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
# 指定 JWT 令牌签名算法的变量 ALGORITHM
ALGORITHM = "HS256"
# 设置令牌过期时间的变量,这里是30分钟
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 假数据,假如这是数据库中的用户表的全量数据
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
# 下面的hash password对应的明文密码为Abc123.
"hashed_password": "$2b$12$oC25Sks9Kg8WU8N3ddoeNugEYYQDQU9Cph7aLvP9VL.uNykdgCQQG",
"disabled": False,
}
}
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(BaseModel):
username: str
email: Union[str, None] = None
full_name: Union[str, None] = None
disabled: Union[bool, None] = None
class UserInDB(User):
hashed_password: str
class TokenData(BaseModel):
username: Union[str, None] = None
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证输入的密码的hash密码与数据库中记录的hash密码是否是一样的
:param plain_password: 用户输入的密码
:param hashed_password: hash密码
:return:
"""
return pwd_context.verify(plain_password, hashed_password)
def get_user(db: dict, username: str) -> UserInDB:
"""模拟在数据库中查找用户,找到之后初始化UserInDB类并返回实例"""
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db: dict, username: str, password: str) -> Union[bool, UserInDB]:
"""
先验证$username用户是否在数据库中存在,存在则继续验证用户输入的明文密码与数据库中记录的hash密码是否匹配
如果都没问题就返回
:param fake_db:
:param username:
:param password:
:return:
"""
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password): # user是UserInDB类的实例,所以可以点属性
return False
return user
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:
"""创建带exp字段的JWT字符串"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta # 这里是utc时间,不是东八区时间
# print(expire) # 2023-01-18 08:14:02.453944
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire}) # datetime.datetime(2023, 1, 18, 8, 14, 02, 453944)
# SECRET_KEY对声明集进行签名的密钥
# jwt.encode()对声明集进行编码并返回 JWT 字符串。
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def get_current_user(token: str) -> Union[UserInDB, None]:
"""
解密JWT,即验证JWT字符串的SIGNATURE签名并返回claims(也称PAYLOAD)的信息
:param token:
:return:
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# print(payload) # {'sub': 'johndoe', 'exp': 1674033230}
username: str = payload.get("sub")
if username is None:
print("username 不存在")
token_data = TokenData(username=username)
user = get_user(fake_users_db, username=token_data.username)
return user
except JWTError as e:
print(e) # Signature verification failed.
if __name__ == '__main__':
# form_data是模拟Request body的参数
form_data = {
"username": "johndoe",
"password": "Abc123."
}
# 创建token过期时间
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# print(access_token_expires) # 0:30:00
# 验证Request body传入的用户是否在数据库中存在
# 如果存在则比对password与数据库中记录的hash password是否匹配
# 最终,没问题则返回该用户的UserInDB类的实例对象user;有问题则返回False
user = authenticate_user(fake_users_db, form_data.get("username"), form_data.get("password"))
print(user)
# 创建PAYLOAD带exp字段、sub字段的JWT字符串
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
print(access_token) # 解码用https://jwt.io/
# 根据JWT获取当前用户
current_user = get_current_user(access_token)
print(current_user)
当然,我们还可以为token添加权限。
划重点,sub
键在整个应用中应该只有一个唯一的标识符,而且应该是字符串。