如何设计和实现一个`RESTful API`,并使用`JWT`(`JSON Web Tokens`)进行`身份`验证。

好的,下面我们开始今天的讲座,主题是“如何设计和实现一个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随着时间的推移而发展,而不会破坏客户端。

二、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 身份验证

  1. 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 (私有声明):在发送者和接收者之间协商的声明。
    • Signature (签名):用于验证令牌的完整性和真实性。签名是通过将头部、载荷和密钥组合在一起,使用头部中指定的算法进行加密生成的。
  2. JWT 工作流程

    • 用户登录:用户提供用户名和密码进行登录。
    • 服务器验证:服务器验证用户的凭据。
    • 生成 JWT:如果凭据有效,服务器生成一个 JWT,其中包含用户的身份信息和其他声明。
    • 返回 JWT:服务器将 JWT 返回给客户端。
    • 客户端存储 JWT:客户端通常将 JWT 存储在本地存储(例如,localStorage、sessionStorage 或 cookies)中。
    • 客户端发送 JWT:客户端在后续的每个请求中,将 JWT 放在请求头中(通常是 Authorization: Bearer <token>)。
    • 服务器验证 JWT:服务器接收到请求后,验证 JWT 的签名和声明。
    • 授权访问:如果 JWT 有效,服务器授权客户端访问受保护的资源。
  3. 代码示例 (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 才能访问。

    测试

    1. 启动 Flask 应用。
    2. 使用 curlPostman 发送 POST 请求到 /login,并提供用户名和密码 (使用 Basic Auth)。
    3. 如果登录成功,你将收到一个 JWT。
    4. 在请求 /protected 时,将 JWT 放在 Authorization 请求头中(例如,Authorization: Bearer <token>)。

四、JWT 的安全性考虑

  • 使用强密钥:使用足够长的随机密钥,并定期更换密钥。
  • 保护密钥:不要将密钥存储在代码库中。使用环境变量或专门的密钥管理系统。
  • 使用 HTTPS:确保所有通信都通过 HTTPS 进行加密。
  • 验证声明:在验证 JWT 时,验证 issaudexp 等声明。
  • 限制 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。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注