flask JWT 认证

发布于:2025-06-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

下面详细介绍如何使用 Flask 后端和 HTML 前端实现 JWT(JSON Web Token)认证系统。

项目概述

这是一个完整的 JWT 认证示例,包含:

  • Flask 后端 API
  • HTML 前端界面
    项目目录
    在这里插入图片描述

后端实现详解

1. 环境准备

首先安装虚拟环境和必要的 Python 库:

uv init
uv venv
source .venv/Script/activate
uv pip install flask flask-cors flask-login itsdangerous

2. 核心组件

用户模型和数据
class User(UserMixin):
    def __init__(self, id, username, email):
        self.id = id
        self.username = username
        self.email = email

# 模拟用户数据库
users_db = {
    'admin': {'id': '1', 'username': 'admin', 'email': 'admin@example.com', 'password': 'password123'},
    'user': {'id': '2', 'username': 'user', 'email': 'user@example.com', 'password': 'password456'}
}
JWT 令牌生成
# JWT 序列化器
jwt_serializer = Serializer(app.config['SECRET_KEY'])

# 生成 JWT 令牌
def generate_token(user_data):
    payload = {
        'user_id': user_data['id'],
        'username': user_data['username']
    }
    token = jwt_serializer.dumps(payload)
    return token
请求加载器(核心认证逻辑)
@login_manager.request_loader
def load_user_from_request(request):
    # 获取 Authorization 头
    authorization = request.headers.get('Authorization')
    
    if not authorization:
        return None
    
    try:
        # 移除 'Bearer ' 前缀(如果有)
        if authorization.startswith('Bearer '):
            token = authorization[7:]
        else:
            token = authorization
        
        # 解析 JWT 令牌
        payload = jwt_serializer.loads(token, max_age=24*3600)  # 24小时有效期
        
        # 根据 payload 创建用户对象
        user = User(
            id=payload['user_id'],
            username=payload['username'],
            email=f"{payload['username']}@example.com"
        )
        
        return user
        
    except Exception as e:
        return None

3. API 端点

登录接口
@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 验证用户凭证
    if username in users_db and users_db[username]['password'] == password:
        user_data = users_db[username]
        
        # 生成 JWT 令牌
        token = generate_token(user_data)
        
        return jsonify({
            'success': True,
            'message': '登录成功!',
            'access_token': token,
            'user': {
                'id': user_data['id'],
                'username': user_data['username'],
                'email': user_data['email']
            }
        })
    
    return jsonify({
        'success': False,
        'message': '用户名或密码错误!'
    }), 401
受保护的接口
@app.route('/api/profile', methods=['GET'])
def get_profile():
    if current_user.is_authenticated:
        return jsonify({
            'success': True,
            'user': {
                'id': current_user.id,
                'username': current_user.username,
                'email': current_user.email
            },
            'message': f'欢迎,{current_user.username}!'
        })
    else:
        return jsonify({
            'success': False,
            'message': '未授权访问,请先登录!'
        }), 401

前端实现详解

1. 核心 JavaScript 功能

通用请求函数
async function makeRequest(url, options = {}) {
    try {
        // 如果有令牌,自动添加 Authorization 头
        if (currentToken) {
            options.headers = {
                'Authorization': `Bearer ${currentToken}`,
                'Content-Type': 'application/json',
                ...options.headers
            };
        } else {
            options.headers = {
                'Content-Type': 'application/json',
                ...options.headers
            };
        }
        
        const response = await fetch(API_BASE + url, options);
        const data = await response.json();
        
        return { success: response.ok, data, status: response.status };
    } catch (error) {
        return { success: false, error: error.message };
    }
}
登录功能
async function login() {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    
    const result = await makeRequest('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password })
    });
    
    if (result.success && result.data.access_token) {
        // 保存令牌到本地存储
        currentToken = result.data.access_token;
        localStorage.setItem('access_token', currentToken);
        updateLoginStatus(true);
    }
    
    showResponse('loginResponse', result);
}

2. 令牌管理

  • 存储:使用 localStorage 持久化存储 JWT 令牌
  • 自动加载:页面加载时检查本地存储的令牌
  • 自动添加:每个 API 请求自动添加 Authorization

完整代码

html 页面

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JWT 认证示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input, button {
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        input {
            width: 200px;
        }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #0056b3;
        }
        button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
        }
        .response {
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 4px;
            padding: 10px;
            margin-top: 10px;
            white-space: pre-wrap;
            font-family: monospace;
            font-size: 12px;
            max-height: 300px;
            overflow-y: auto;
        }
        .success {
            color: #28a745;
        }
        .error {
            color: #dc3545;
        }
        .token-display {
            background-color: #e9ecef;
            padding: 10px;
            border-radius: 4px;
            word-break: break-all;
            font-family: monospace;
            font-size: 11px;
        }
        .status {
            padding: 5px 10px;
            border-radius: 4px;
            font-weight: bold;
            display: inline-block;
            margin-bottom: 10px;
        }
        .status.logged-in {
            background-color: #d4edda;
            color: #155724;
        }
        .status.logged-out {
            background-color: #f8d7da;
            color: #721c24;
        }
    </style>
</head>
<body>
    <h1>JWT 认证示例 - Authorization 头演示</h1>
    
    <!-- 登录状态显示 -->
    <div class="container">
        <h2>登录状态</h2>
        <div id="loginStatus" class="status logged-out">未登录</div>
        <div id="tokenDisplay" class="token-display" style="display: none;">
            <strong>当前 JWT 令牌:</strong><br>
            <span id="currentToken"></span>
        </div>
    </div>
    
    <!-- 登录表单 -->
    <div class="container">
        <h2>用户登录</h2>
        <div class="form-group">
            <label>用户名:</label>
            <input type="text" id="username" value="admin" placeholder="admin 或 user">
        </div>
        <div class="form-group">
            <label>密码:</label>
            <input type="password" id="password" value="password123" placeholder="password123 或 password456">
        </div>
        <button onclick="login()">登录</button>
        <button onclick="logout()">登出</button>
        <div id="loginResponse" class="response"></div>
    </div>
    
    <!-- API 测试 -->
    <div class="container">
        <h2>API 测试(需要 Authorization 头)</h2>
        <button onclick="getProfile()" id="profileBtn" disabled>获取用户资料</button>
        <button onclick="testHeaders()" id="headersBtn" disabled>测试请求头</button>
        <div id="apiResponse" class="response"></div>
    </div>
    
    <!-- 请求示例 -->
    <div class="container">
        <h2>请求示例代码</h2>
        <div id="requestExample" class="response">
// 前端发送带 Authorization 头的请求示例:

fetch('http://localhost:5000/api/profile', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
        'Content-Type': 'application/json'
    }
})
.then(response => response.json())
.then(data => console.log(data));
        </div>
    </div>

    <script>
        const API_BASE = 'http://localhost:5000';
        let currentToken = null;
        
        // 页面加载时检查本地存储的令牌
        window.onload = function() {
            const token = localStorage.getItem('access_token');
            if (token) {
                currentToken = token;
                updateLoginStatus(true);
            }
        };
        
        // 更新登录状态显示
        function updateLoginStatus(isLoggedIn) {
            const statusElement = document.getElementById('loginStatus');
            const tokenDisplay = document.getElementById('tokenDisplay');
            const currentTokenElement = document.getElementById('currentToken');
            const profileBtn = document.getElementById('profileBtn');
            const headersBtn = document.getElementById('headersBtn');
            
            if (isLoggedIn && currentToken) {
                statusElement.textContent = '已登录';
                statusElement.className = 'status logged-in';
                tokenDisplay.style.display = 'block';
                currentTokenElement.textContent = currentToken;
                profileBtn.disabled = false;
                headersBtn.disabled = false;
            } else {
                statusElement.textContent = '未登录';
                statusElement.className = 'status logged-out';
                tokenDisplay.style.display = 'none';
                profileBtn.disabled = true;
                headersBtn.disabled = true;
            }
        }
        
        // 通用请求函数
        async function makeRequest(url, options = {}) {
            try {
                // 如果有令牌,自动添加 Authorization 头
                if (currentToken) {
                    options.headers = {
                        'Authorization': `Bearer ${currentToken}`,
                        'Content-Type': 'application/json',
                        ...options.headers
                    };
                } else {
                    options.headers = {
                        'Content-Type': 'application/json',
                        ...options.headers
                    };
                }
                
                console.log('🚀 发送请求:', {
                    url: API_BASE + url,
                    method: options.method || 'GET',
                    headers: options.headers
                });
                
                const response = await fetch(API_BASE + url, options);
                const data = await response.json();
                
                return { success: response.ok, data, status: response.status };
            } catch (error) {
                return { success: false, error: error.message };
            }
        }
        
        // 显示响应
        function showResponse(elementId, result) {
            const element = document.getElementById(elementId);
            if (result.success) {
                element.className = 'response success';
                element.textContent = JSON.stringify(result.data, null, 2);
            } else {
                element.className = 'response error';
                element.textContent = result.error || JSON.stringify(result.data, null, 2);
            }
        }
        
        // 登录
        async function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            
            const result = await makeRequest('/api/login', {
                method: 'POST',
                body: JSON.stringify({ username, password })
            });
            
            if (result.success && result.data.access_token) {
                // 保存令牌到本地存储
                currentToken = result.data.access_token;
                localStorage.setItem('access_token', currentToken);
                updateLoginStatus(true);
                
                console.log('✅ 登录成功,令牌已保存:', currentToken.substring(0, 50) + '...');
            }
            
            showResponse('loginResponse', result);
        }
        
        // 登出
        function logout() {
            currentToken = null;
            localStorage.removeItem('access_token');
            updateLoginStatus(false);
            
            document.getElementById('loginResponse').textContent = '已登出';
            document.getElementById('apiResponse').textContent = '';
            
            console.log('👋 已登出');
        }
        
        // 获取用户资料(需要 Authorization 头)
        async function getProfile() {
            if (!currentToken) {
                alert('请先登录!');
                return;
            }
            
            const result = await makeRequest('/api/profile');
            showResponse('apiResponse', result);
        }
        
        // 测试请求头
        async function testHeaders() {
            const result = await makeRequest('/api/test-headers');
            showResponse('apiResponse', result);
        }
    </script>
</body>
</html>

backend_jwt_example.py

from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_login import LoginManager, UserMixin, login_user, current_user
from itsdangerous import URLSafeTimedSerializer as Serializer
import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

# 启用 CORS
CORS(app, supports_credentials=True)

# 初始化 LoginManager
login_manager = LoginManager()
login_manager.init_app(app)

# 模拟用户数据
class User(UserMixin):
    def __init__(self, id, username, email):
        self.id = id
        self.username = username
        self.email = email

users_db = {
    'admin': {'id': '1', 'username': 'admin', 'email': 'admin@example.com', 'password': 'password123'},
    'user': {'id': '2', 'username': 'user', 'email': 'user@example.com', 'password': 'password456'}
}

# 存储有效的 access_token
valid_tokens = {}

# JWT 序列化器
jwt_serializer = Serializer(app.config['SECRET_KEY'])

# 生成 JWT 令牌
def generate_token(user_data):
    payload = {
        'user_id': user_data['id'],
        'username': user_data['username']
    }
    token = jwt_serializer.dumps(payload)
    return token

# request_loader - 从 Authorization 头获取用户
@login_manager.request_loader
def load_user_from_request(request):
    # 获取 Authorization 头
    authorization = request.headers.get('Authorization')
    
    if not authorization:
        return None
    
    try:
        # 移除 'Bearer ' 前缀(如果有)
        if authorization.startswith('Bearer '):
            token = authorization[7:]
        else:
            token = authorization
        
        # 解析 JWT 令牌
        payload = jwt_serializer.loads(token, max_age=24*3600)  # 24小时有效期
        
        # 根据 payload 创建用户对象
        user = User(
            id=payload['user_id'],
            username=payload['username'],
            email=f"{payload['username']}@example.com"
        )
        
        print(f"✅ 成功验证用户: {user.username}")
        return user
        
    except Exception as e:
        print(f"❌ JWT 验证失败: {e}")
        return None

# 登录接口
@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # 验证用户凭证
    if username in users_db and users_db[username]['password'] == password:
        user_data = users_db[username]
        
        # 生成 JWT 令牌
        token = generate_token(user_data)
        
        return jsonify({
            'success': True,
            'message': '登录成功!',
            'access_token': token,
            'user': {
                'id': user_data['id'],
                'username': user_data['username'],
                'email': user_data['email']
            }
        })
    
    return jsonify({
        'success': False,
        'message': '用户名或密码错误!'
    }), 401

# 受保护的接口 - 需要 Authorization 头
@app.route('/api/profile', methods=['GET'])
def get_profile():
    if current_user.is_authenticated:
        return jsonify({
            'success': True,
            'user': {
                'id': current_user.id,
                'username': current_user.username,
                'email': current_user.email
            },
            'message': f'欢迎,{current_user.username}!'
        })
    else:
        return jsonify({
            'success': False,
            'message': '未授权访问,请先登录!'
        }), 401

# 测试接口 - 显示请求头信息
@app.route('/api/test-headers', methods=['GET'])
def test_headers():
    headers = dict(request.headers)
    authorization = request.headers.get('Authorization')
    
    return jsonify({
        'authorization_header': authorization,
        'all_headers': headers,
        'is_authenticated': current_user.is_authenticated,
        'current_user': current_user.username if current_user.is_authenticated else None
    })

if __name__ == '__main__':
    print("\n=== JWT 认证示例启动 ===")
    print("测试用户:")
    print("  - 用户名: admin, 密码: password123")
    print("  - 用户名: user, 密码: password456")
    print("\n接口说明:")
    print("  - POST /api/login - 登录获取令牌")
    print("  - GET /api/profile - 获取用户资料(需要 Authorization 头)")
    print("  - GET /api/test-headers - 查看请求头信息")
    print("========================\n")
    
    app.run(debug=True, host='0.0.0.0', port=5000)

运行步骤

1. 启动后端服务

uv run backend_jwt_example.py

服务将在 http://localhost:5000 启动。

2. 打开前端页面

在浏览器中打开 http://localhost:5000/static/frontend_jwt_example.html 文件。

3. 测试功能

  1. 登录测试

    • 用户名:admin,密码:password123
    • 用户名:user,密码:password456
  2. API 测试

    • 登录成功后,点击"获取用户资料"按钮
    • 点击"测试请求头"查看认证信息
      在这里插入图片描述

关键特性

1. Authorization 头认证

  • 前端自动在请求头中添加 Authorization: Bearer <token>
  • 后端通过 request_loader 自动解析和验证令牌

2. 令牌生命周期管理

  • 令牌有效期:24小时
  • 自动过期处理
  • 本地存储持久化

3. 安全特性

  • CORS 支持跨域请求
  • 令牌签名验证
  • 自动过期检查

4. 用户体验

  • 实时登录状态显示
  • 令牌可视化
  • 详细的错误信息
  • 请求示例代码展示