HTTP请求鉴权之JWT

本文将会简单介绍什么是HTTP请求中的JWT鉴权以及如何在实际项目中使用JWT。

欢迎关注我的公众号NLP奇幻之旅,原创技术文章第一时间推送。

欢迎关注我的知识星球“自然语言处理奇幻之旅”,笔者正在努力构建自己的技术社区。

什么是JWT?

什么是HTTP请求鉴权?

在介绍HTTP请求中的JWT鉴权前,我们首先需要了解什么是HTTP请求中的鉴权。

在我们日常使用互联网服务时,常常离不开用户认证,这里面其实就是HTTP请求鉴权。

HTTP请求中的鉴权(Authentication/Authorization)是指:在客户端(如浏览器、App、API调用者)访问受保护资源时,服务端对其身份的确认与权限的验证。它主要分为两部分:认证授权。所谓认证,指的是客户端需要提供自己的“身份凭证”,服务端根据凭证确认身份是否合法。而授权,指的是即使认证通过,服务端还要判断:你是否能访问这个接口或资源?

目前我们常用的鉴权有四种:

  • HTTP Basic Authentication
  • session-cookie
  • Token 验证
  • OAuth(开放授权)

本文将会介绍的JWT鉴权属于Token 验证。

JWT原理

JWT 的全称为JSON Web Token,其基本原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

1
2
3
4
5
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。

JWT结构

实际上我们使用 JWT 形似如下的字符串:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ

它由 . 隔开,分为三部分:Header(头部)、Payload(负载)、Signature(签名)。

jwt_00.png

Header部分经过解析后是一个 JSON 对象,用来描述 JWT 的元数据,一般结构如下:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256;typ属性表示 token 的类型(type)。将 JSON 对象使用 Base64URL 算法转成字符串。

Payload部分经过解析后也是一个 JSON 对象,用来存放用户身份相关数据。例如:

1
2
3
4
5
6
{
"sub": "1234567890",
"name": "George White",
"admin": true,
"iat": 1516239022
}

将上面的 JSON 对象使用 Base64URL 算法转成字符串,Header部分和Payload部均采用这个算法实现。

官方给出的Payload部分的7个标准字段如下:

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

Signature 部分则是通过加盐加密得到的,加密由服务端实现,实现方式如下图:

signature 算法

简单来说,就是把 HeaderPayload 部分进行 base64编码,用点拼接起来,得到结果,把这个结果用我们设置的密钥和算法进行加盐加密,就得到了 Signature 部分。

Python 实现 JWT

在Python语言中,有不少第三方模块提供了JWT的实现,我们以python-jose模块为例,来演示如何使用JWT。

首先是使用该模块生成和验证JWT,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用jose库生成和验证JWT
# pip install python-jose==3.5.0

from jose import jwt

# 创建Payload
payload = {"user_id": "1234", "user_name": "Python"}

# 签名密钥
secret_key = "jc2025"

# 生成JWT
token = jwt.encode(payload, secret_key, algorithm='HS256')

print("JWT Token:", token)

# 验证JWT
decoded_token = jwt.decode(token, secret_key, algorithms=['HS256'])
print("Decoded Payload:", decoded_token)

输出结果如下:

1
2
JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNCIsInVzZXJfbmFtZSI6IlB5dGhvbiJ9.IzJ1AplGgQzPuOO2RRnx_PscdwL1efZw8FRa0bjpIKs
Decoded Payload: {'user_id': '1234', 'user_name': 'Python'}

有不少网站也提供了JWT的解析,比如网站 https://jwt.io/ ,这个网站提供的 JWT 认证功能简单实用。我们将上面的JWT Token在这个网站进行解析,如下图:

jwt_5.png

我们将JWT Token粘贴至左侧的ENCODE VALUE,右侧就能自动解析出 HeaderPayload 部分,但 Signature 部分需要我们提供正确的秘钥(SECRET)才能验证。

我们在上面的JWT的生成和验证功能基础上,再引入一个超时机制,这也是 JWT 常见的使用方式。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 在payload中加入过期时间exp为1分钟,然后验证
import time
from jose import jwt
from datetime import datetime, timedelta, timezone

# 创建带过期时间的payload
exp_payload = {
"user_id": "1234",
"user_name": "Python",
"exp": datetime.now(timezone.utc) + timedelta(minutes=1) # 1分钟后过期
}
secret_key = "jc2025"

# 生成带过期时间的JWT
exp_token = jwt.encode(exp_payload, secret_key, algorithm='HS256')

print("\n带过期时间的JWT Token:", exp_token)

try:
decode_secret_key = secret_key
# decode_secret_key = secret_key + "2"
# time.sleep(62)
# 验证带过期时间的JWT
decoded_exp_token = jwt.decode(exp_token, decode_secret_key, algorithms=['HS256'])
print("解码的Payload:", decoded_exp_token)
except jwt.ExpiredSignatureError:
print("Token已过期!")
except jwt.JWTError:
print("Token验证失败!")

在上述代码中,我们在 Payload部分加入了超时机制。

  • 如果我们使用正确的secret key且使用时间不超过1分钟,则能正确解析
  • 如果使用错误的secret key,则会报错:Token验证失败!
  • 如果使用了正确的secret key,但使用时间超时,则会报错:Token已过期!

上述三种情况的输出分别如下:

1
2
3
4
5
6
7
8
带过期时间的JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNCIsInVzZXJfbmFtZSI6IlB5dGhvbiIsImV4cCI6MTc1MTI5MTc2MX0.jMrINdHTy56q19N--P8rLtgBpb2wmFamYaeErEXiMEA
解码的Payload: {'user_id': '1234', 'user_name': 'Python', 'exp': 1751291761}

带过期时间的JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNCIsInVzZXJfbmFtZSI6IlB5dGhvbiIsImV4cCI6MTc1MTI5MTc4Mn0.W2ecyHE7YaNH58P87dZ5aXqRq9Fy1BAITtrEDtMgSrA
Token验证失败!

带过期时间的JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNCIsInVzZXJfbmFtZSI6IlB5dGhvbiIsImV4cCI6MTc1MTI5MTgxMX0.Kh1StJcVtq59oAvodh_Ul_OOOZFmXWxfKLzBLfold0I
Token已过期!

上述Python代码虽然简单,但对我们理解 JWT机制是非常有帮助的。

在FastAPI中使用JWT

成熟的Python Web框架,比如 Flask, FastAPI都对 JWT提供了很好地支持。本节将会介绍如何在FastAPI中使用JWT。

我们使用 FastAPI 实现一个简单的 JWT 服务,用于用户认证,并对相应的 API进行鉴权。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 在fastapi中使用jwt - 简化版本
# 主要功能: 实现基础的用户登录和JWT认证演示

from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import JWTError, jwt, ExpiredSignatureError

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# 密钥和算法配置
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 10


# 用户模型
class User(BaseModel):
username: str
email: str | None = None


class Token(BaseModel):
access_token: str
token_type: str


# 模拟用户数据库(简化版)
fake_users_db = {
"admin": {
"username": "admin",
"email": "admin@example.com",
"password": "admin123" # 实际应用中应该存储哈希密码
},
"user": {
"username": "user",
"email": "user@example.com",
"password": "user123"
}
}


# 验证用户登录
def authenticate_user(username: str, password: str):
user = fake_users_db.get(username)
if not user:
return False
if user["password"] != password: # 实际应用中应该验证哈希密码
return False
return user


# 创建JWT token
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


# 获取当前用户
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无法验证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
expired_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token已过期",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except ExpiredSignatureError:
raise expired_exception
except JWTError:
raise credentials_exception

user = fake_users_db.get(username)
if user is None:
raise credentials_exception
return User(username=user["username"], email=user["email"])


# 登录接口
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}


# 需要认证的接口示例
@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user


# 受保护的资源接口示例
@app.get("/protected")
async def protected_route(current_user: User = Depends(get_current_user)):
return {"message": f"你好 {current_user.username},这是一个受保护的接口!"}


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

启动上述服务,/token 接口提供了对应用户的JWT Token, /users/me 接口使用JWT认证,用于获取对应用户信息,/protected 接口用于演示受保护的资源。

上面使用JWT机制的Python代码应该不难理解。

我们使用两种工具来测试接口服务,一种是Postman工具,一种是FastAPI自带的Swagger页面。

  • Postman

以Postman工具为例,/token 接口测试如下:

/token 接口

在 Authorization 页面填入上述JWT Token,请求 /users/me 接口,返回结果如下:

正确的JWT Token

如果不输入Token,则请求结果如下:

未输入 Token

  • Swagger页面

使用FastAPI自带的Swagger页面,测试接口将会非常方便。

授权页面如下:

请求接口如下:

JWT实战

JWT 尝尝用于用户认证,因此,一般需要用户认证的接口都可以用 JWT机制来授权与认证。

最近,AI Coding非常火爆,笔者在日常工作中使用的工具为Cursor。笔者使用Cursor写了一个JWT实战项目,基于FastAPI和MySQL的现代化学生成绩管理系统,支持JWT身份认证,具有老师和学生两种角色权限管理。该项目已上传至Github,网址为:https://github.com/percent4/student_grade_management

这里不再过多介绍这个项目的实现代码和具体的用户身份认证,只讲下这个实战项目的主要功能,有兴趣的读者可以自行阅读该项目代码。该项目的主要功能如下:

  • 🔐 安全认证: JWT令牌认证,bcrypt密码加密
  • 👥 角色管理: 老师和学生角色,权限分离
  • 📊 成绩管理: 支持语文、数学、英语三科成绩录入与查看
  • 🔧 密码管理: 用户可自主修改初始密码
  • 📱 响应式界面: 现代化UI设计,支持移动端访问
  • 🚀 实时更新: 成绩修改实时生效,数据同步更新
  • 🛡️ 数据安全: 完整的输入验证和权限控制

主要功能的页面截图如下:

用户登录页面

老师管理成绩页面

学生查看成绩页面

总结

本文主要介绍了什么是HTTP请求中的鉴权,JWT的基本原理和结构,并简单演示了如何使用Python来生成和验证JWT。

在Python的Web框架中,实现JWT用户身份认证是非常方便的,本文也给出了这方面的例子。

最后,笔者使用Cursor,实现了一个学生成绩管理系统,其中的用户身份认证用到了JWT机制。

如果你能完整地读完上面的文章,并对上述Python代码认真阅读与测试,相信你一定能掌握JWT方面的基础知识了。

本文到此结束,后续笔者将会持续更新HTTP鉴权方面的文章~

参考文献

  1. 前后端常见的几种鉴权方式: https://juejin.cn/post/6844903927100473357
  2. JWT 认证及其在 FastAPI 中的使用: https://krau.top/posts/fastapi-jwt
  3. JSON Web Token 入门教程: https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
  4. JWT(JSON Web Token)原理、使用方法及使用注意事项: https://zhuanlan.zhihu.com/p/662299933
  5. JWT 验证工具的网站: https://jwt.io/