好的,下面我们开始今天的讲座,主题是“如何设计和实现一个RESTful API,并使用JWT进行身份验证”。
一、RESTful API 设计原则
在深入JWT之前,我们需要确保我们的API遵循RESTful原则。这不仅能提高API的可维护性和可扩展性,还能使其更易于理解和使用。以下是一些关键的RESTful原则:
-
客户端-服务器 (Client-Server):客户端和服务器分离,客户端不应该关心数据的存储方式,服务器也不应该关心客户端的UI。
-
无状态 (Stateless):服务器不应存储任何关于客户端状态的信息。每个请求都应包含处理该请求所需的所有信息。
-
可缓存 (Cacheable):服务器响应应该明确指示是否可以缓存,以及缓存多久。
-
分层系统 (Layered System):客户端无法判断它是直接连接到服务器,还是通过中间层连接。
-
按需代码 (Code on Demand) (可选):服务器可以向客户端发送可执行代码,以扩展客户端的功能。
-
统一接口 (Uniform Interface):这是RESTful API的核心原则,包括以下子原则:
- 资源识别 (Resource Identification):使用URI来标识资源。例如,
/users/123
标识ID为123的用户资源。 - 通过表述操作资源 (Manipulation of Resources Through Representations):客户端通过表述(例如,JSON)来操作资源。例如,使用PUT请求和JSON数据来更新用户资源。
- 自描述消息 (Self-Descriptive Messages):每个消息都应包含足够的信息来让客户端理解如何处理它。例如,使用Content-Type头部来指示消息的格式。
- 超媒体作为应用程序状态引擎 (Hypermedia as the Engine of Application State – HATEOAS):使用超媒体链接来引导客户端浏览API。这允许API随着时间的推移而发展,而不会破坏客户端。
- 资源识别 (Resource Identification):使用URI来标识资源。例如,
二、API 端点设计
假设我们要构建一个用户管理API。以下是一些常用的端点设计示例:
HTTP 方法 | 端点 | 描述 | 请求体示例 (JSON) | 响应体示例 (JSON) |
---|---|---|---|---|
POST | /users |
创建新用户 | {"username": "john", "email": "[email protected]", "password": "secret"} |
{"id": 123, "username": "john", "email": "[email protected]"} |
GET | /users/{id} |
获取指定ID的用户信息 | 无 | {"id": 123, "username": "john", "email": "[email protected]"} |
PUT | /users/{id} |
更新指定ID的用户信息 | {"email": "[email protected]"} |
{"id": 123, "username": "john", "email": "[email protected]"} |
DELETE | /users/{id} |
删除指定ID的用户 | 无 | 无 (或可以返回一个成功的状态码) |
GET | /users |
获取所有用户 (可以支持分页、排序等) | 无 (或可以包含查询参数) | [{"id": 123, "username": "john", "email": "[email protected]"}, {"id": 456, "username": "jane", "email": "[email protected]"}] |
POST | /login |
用户登录,返回 JWT token | {"username": "john", "password": "secret"} |
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"} |
三、JWT 身份验证
-
JWT 概念
JWT(JSON Web Token)是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT 是紧凑的、URL安全的,并且可以包含任何JSON数据,这些数据可以被验证和信任,因为它是经过数字签名的。
一个JWT主要由三部分组成:
- Header (头部):指定令牌的类型和使用的签名算法。
- Payload (载荷):包含声明(claims),即关于实体(用户)和其他数据的声明。声明可以分为三种类型:
- Registered claims (注册声明):预定义的声明,如
iss
(签发者)、exp
(过期时间)、sub
(主题)、aud
(受众)等。 - Public claims (公共声明):可以由使用者自定义的声明。
- Private claims (私有声明):在发送者和接收者之间协商的声明。
- Registered claims (注册声明):预定义的声明,如
- Signature (签名):用于验证令牌的完整性和真实性。签名是通过将头部、载荷和密钥组合在一起,使用头部中指定的算法进行加密生成的。
-
JWT 工作流程
- 用户登录:用户提供用户名和密码进行登录。
- 服务器验证:服务器验证用户的凭据。
- 生成 JWT:如果凭据有效,服务器生成一个 JWT,其中包含用户的身份信息和其他声明。
- 返回 JWT:服务器将 JWT 返回给客户端。
- 客户端存储 JWT:客户端通常将 JWT 存储在本地存储(例如,localStorage、sessionStorage 或 cookies)中。
- 客户端发送 JWT:客户端在后续的每个请求中,将 JWT 放在请求头中(通常是
Authorization: Bearer <token>
)。 - 服务器验证 JWT:服务器接收到请求后,验证 JWT 的签名和声明。
- 授权访问:如果 JWT 有效,服务器授权客户端访问受保护的资源。
-
代码示例 (Python Flask)
以下是一个使用 Python Flask 和
PyJWT
库实现 JWT 身份验证的示例。安装依赖
pip install Flask PyJWT
代码
import jwt import datetime from functools import wraps from flask import Flask, request, jsonify, make_response app = Flask(__name__) app.config['SECRET_KEY'] = 'your_secret_key' # 建议使用更强的密钥 # 模拟用户数据 users = { 'john': {'password': 'secret'} } # JWT 创建函数 def generate_token(username): payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30), # 设置token 过期时间 'iat': datetime.datetime.utcnow(), 'sub': username } token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') return token # JWT 验证装饰器 def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = None if 'Authorization' in request.headers: auth_header = request.headers['Authorization'] try: token = auth_header.split(" ")[1] except: return jsonify({'message': 'Invalid token format!'}), 401 if not token: return jsonify({'message': 'Token is missing!'}), 401 try: data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) current_user = data['sub'] # 假设 'sub' 声明包含用户名 except jwt.ExpiredSignatureError: return jsonify({'message': 'Token has expired!'}), 401 except jwt.InvalidTokenError: return jsonify({'message': 'Token is invalid!'}), 401 return f(current_user, *args, **kwargs) # 将用户名传递给被装饰的函数 return decorated # 登录路由 @app.route('/login', methods=['POST']) def login(): auth = request.authorization if not auth or not auth.username or not auth.password: return make_response('Could not verify', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) user = users.get(auth.username) if not user or user['password'] != auth.password: return make_response('Could not verify', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) token = generate_token(auth.username) return jsonify({'token': token}) # 受保护的路由 @app.route('/protected', methods=['GET']) @token_required def protected(current_user): return jsonify({'message': f'Hello, {current_user}! This is a protected resource.'}) if __name__ == '__main__': app.run(debug=True)
代码解释
generate_token(username)
:生成 JWT,包含用户名和过期时间。token_required(f)
:装饰器,用于验证 JWT。 它从请求头中提取token,如果token不存在或无效,则返回错误。如果token有效,则解码token并提取用户名,然后将用户名传递给被装饰的函数。/login
:登录路由,验证用户名和密码,如果验证成功,则生成 JWT 并返回。/protected
:受保护的路由,需要有效的 JWT 才能访问。
测试
- 启动 Flask 应用。
- 使用
curl
或Postman
发送 POST 请求到/login
,并提供用户名和密码 (使用 Basic Auth)。 - 如果登录成功,你将收到一个 JWT。
- 在请求
/protected
时,将 JWT 放在Authorization
请求头中(例如,Authorization: Bearer <token>
)。
四、JWT 的安全性考虑
- 使用强密钥:使用足够长的随机密钥,并定期更换密钥。
- 保护密钥:不要将密钥存储在代码库中。使用环境变量或专门的密钥管理系统。
- 使用 HTTPS:确保所有通信都通过 HTTPS 进行加密。
- 验证声明:在验证 JWT 时,验证
iss
、aud
、exp
等声明。 - 限制 JWT 的有效期:设置合适的过期时间,避免 JWT 被长期滥用。
- 防止重放攻击:可以使用 JTI (JWT ID) 声明来防止重放攻击。
- 避免在 JWT 中存储敏感信息:JWT 是可解码的,因此不要在其中存储密码或其他敏感信息。
- 使用适当的算法:HS256 (HMAC with SHA-256) 是一种常用的算法,但如果可能,可以考虑使用非对称算法(例如,RS256),以便更好地保护密钥。
五、 Refresh Token 的使用
JWT 的有效期通常较短,为了避免用户频繁登录,可以使用 Refresh Token 机制。
- 用户登录:用户提供用户名和密码进行登录。
- 服务器验证:服务器验证用户的凭据。
- 生成 JWT 和 Refresh Token:如果凭据有效,服务器生成一个 JWT (有效期较短) 和一个 Refresh Token (有效期较长)。
- 返回 JWT 和 Refresh Token:服务器将 JWT 和 Refresh Token 返回给客户端。
- 客户端存储 JWT 和 Refresh Token:客户端将 JWT 存储在内存中,将 Refresh Token 存储在持久化存储中(例如,localStorage 或 cookies)。
- 客户端发送 JWT:客户端在后续的每个请求中,将 JWT 放在请求头中。
- 服务器验证 JWT:服务器接收到请求后,验证 JWT。
- JWT 过期:当 JWT 过期时,客户端使用 Refresh Token 向服务器请求新的 JWT。
- 服务器验证 Refresh Token:服务器验证 Refresh Token 的有效性。
- 生成新的 JWT:如果 Refresh Token 有效,服务器生成一个新的 JWT。
- 返回新的 JWT:服务器将新的 JWT 返回给客户端。
示例 (Python Flask)
import jwt
import datetime
import uuid
from functools import wraps
from flask import Flask, request, jsonify, make_response
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key' # 建议使用更强的密钥
app.config['REFRESH_SECRET_KEY'] = 'your_refresh_secret_key' # 建议使用更强的密钥
# 模拟用户数据
users = {
'john': {'password': 'secret', 'refresh_token': None}
}
# JWT 创建函数
def generate_token(username):
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15), # JWT 有效期短
'iat': datetime.datetime.utcnow(),
'sub': username
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
return token
# Refresh Token 创建函数
def generate_refresh_token(username):
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30), # Refresh Token 有效期长
'iat': datetime.datetime.utcnow(),
'sub': username,
'jti': str(uuid.uuid4()) # 唯一标识符,防止重放攻击
}
token = jwt.encode(payload, app.config['REFRESH_SECRET_KEY'], algorithm='HS256')
return token
# JWT 验证装饰器
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
try:
token = auth_header.split(" ")[1]
except:
return jsonify({'message': 'Invalid token format!'}), 401
if not token:
return jsonify({'message': 'Token is missing!'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = data['sub'] # 假设 'sub' 声明包含用户名
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired!'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Token is invalid!'}), 401
return f(current_user, *args, **kwargs) # 将用户名传递给被装饰的函数
return decorated
# 登录路由
@app.route('/login', methods=['POST'])
def login():
auth = request.authorization
if not auth or not auth.username or not auth.password:
return make_response('Could not verify', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
user = users.get(auth.username)
if not user or user['password'] != auth.password:
return make_response('Could not verify', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
token = generate_token(auth.username)
refresh_token = generate_refresh_token(auth.username)
users[auth.username]['refresh_token'] = refresh_token # 存储 refresh token
return jsonify({'token': token, 'refresh_token': refresh_token})
# 刷新 Token 路由
@app.route('/refresh', methods=['POST'])
def refresh():
refresh_token = request.json.get('refresh_token')
if not refresh_token:
return jsonify({'message': 'Refresh token is missing!'}), 400
try:
data = jwt.decode(refresh_token, app.config['REFRESH_SECRET_KEY'], algorithms=['HS256'])
current_user = data['sub']
user = users.get(current_user)
if not user or user['refresh_token'] != refresh_token:
return jsonify({'message': 'Invalid refresh token!'}), 401
token = generate_token(current_user)
return jsonify({'token': token})
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Refresh token has expired!'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Refresh token is invalid!'}), 401
# 受保护的路由
@app.route('/protected', methods=['GET'])
@token_required
def protected(current_user):
return jsonify({'message': f'Hello, {current_user}! This is a protected resource.'})
if __name__ == '__main__':
app.run(debug=True)
六、结论
今天,我们讨论了如何设计和实现一个RESTful API,并使用JWT进行身份验证。我们学习了RESTful API的设计原则、API端点的设计,以及JWT的概念、工作流程和安全性考虑。此外,我们还探讨了如何使用Refresh Token来提高用户体验。希望这些内容能帮助你构建更安全、更可靠的API。
总而言之:
掌握 RESTful API 的设计原则与 JWT 的应用是现代 Web 开发的基础。从API设计到token生成与验证,再到Refresh Token的使用,每一个环节都至关重要。确保安全性,并根据需求灵活调整,就能构建出健壮的API。