一、JWT简介
JWT是什么?
-
JWT 即JSON 网络令牌(JSON Web Tokens)。
-
JWT(JSON Web Token) 是一种用于在身份提供者和服务提供者之间传递身份验证和授权数据的开放标准。JWT是一个JSON对象,其中包含了被签名的声明。这些声明可以是身份验证的声明、授权的声明等。JWT可以使用数字签名进行签名,以确保它不被篡改。
-
JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
-
JWT 字符串没有加密,任何人都能用它恢复原始信息。
jwt官网及在线解码 -
但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。
-
例如:使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。
JWT由三部分组成
JWT的格式为:
xxx.xxx.xxx
- Header: 这部分包含了JWT的类型和签名所使用的算法。
- Payload: 也可以叫claims,这部分包含了JWT的声明,例如身份验证的声明和授权的声明。
- Signature: 这部分是使用算法和密钥对前两部分进行签名得到的。这部分用于验证JWT的发送者是否可信任。
Bearer JWT 和 JWT 不是同一个意思
-
JWT (JSON Web Token) 是一种用于在双方之间传递身份验证信息的标准。它是一个 JSON 对象,包含了被签名的声明。这些声明可以是身份验证的信息,比如用户名和密码,也可以是其他与此有关的信息,比如用户的权限。
-
Bearer JWT 是 JWT 的一种使用方式。在这种方式中,JWT 被用作认证授权的令牌,并通过 HTTP 请求的
Authorization
头部进行发送。这个头部的值是Bearer空格
加上 JWT。 -
所以 JWT 是一种身份验证和授权的标准, Bearer JWT 则是 JWT在HTTP请求中的一种传递方式。
进行JWT验证主要有以下几步
- 验证JWT签名:使用JWT的签名算法和密钥对JWT的签名部分进行验证,以确保JWT的发送者是可信任的。
- 验证JWT的有效期:使用JWT中的exp(expiration)和nbf(not before)声明来验证JWT是否在有效期内。
- 验证JWT的权限:使用JWT中的scope声明来验证用户是否有请求资源的权限。
- 验证JWT的其他声明:使用JWT中的其他声明来验证用户是否符合其他条件。
openssl rand -hex 32 什么意思?
openssl rand -hex 32
是一个命令行命令,它使用 OpenSSL 库生成 32 个字符的十六进制随机字符串。
- ‘openssl’ 是一种常用的工具,用于实现各种安全协议和算法,包括密钥生成、证书管理等。
- ‘rand’ 是 OpenSSL 库的一个子命令,它用于生成随机数。
- ‘-hex’ 指示 rand 子命令使用十六进制输出结果。
- ‘32’ 是随机字符串的长度。
所以这个命令就是生成32位十六进制随机字符串。这种随机字符串常用来做盐值,可以用来加密密码,来防止密码被破解。
例如:
$ openssl rand -hex 32
> 6b48014c061a82bb3c6fd4812777552cf41a79c38149e31516b818a73d50ee51
- 这里主要用于JWT的Signature,以确保JWT的发送者是可信任的。即创建用于 JWT 令牌签名的随机密钥。
安装 python-jose
-
python-jose包用于在Python中 生成和校验 JWT 令牌
-
python-jose官网
-
python-jose需要安装配套的加密后端,例如下面我们使用的后端是pyca/cryptography
$ pip3 install "python-jose[cryptography]"
- 除了python-jose,相似的还有PyJWT,但还是推荐Python-jose ,因为支持 PyJWT 的所有功能,还支持与其它工具集成时可能会用到的一些其它功能。
二、密码哈希
哈希是指把特定内容(本例中为密码)转换为乱码形式的字节序列(其实就是字符串)。
每次传入完全相同的内容时(比如,完全相同的密码),返回的都是完全相同的乱码。
但这个乱码无法转换回传入的密码。
为什么使用密码哈希
原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。
这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大)。
bcrypt算法是什么?
-
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
三、Python实现用户验证的完整流程(用于生产环境)
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": "johndoe@example.com",
# 下面的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密码是否匹配
如果都没问题就返回<class '__main__.UserInDB'>
: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) # <class 'datetime.timedelta'> 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
键在整个应用中应该只有一个唯一的标识符,而且应该是字符串。