JWT认证后首次访问/fanbase返回401,刷新后正常的问题排查
问题描述
基于Flask-JWT-Extended实现JWT认证时遇到异常:用户提交登录表单后,服务器返回200并生成JWT token,控制台显示token已存入localStorage,但首次访问受保护的/fanbase接口时,请求未携带Authorization Header,服务器返回401(提示“Missing Authorization Header”);刷新首页后再次点击访问该接口,token正常携带,服务器返回200。
服务器错误日志
127.0.0.1 - - [05/Oct/2023 12:15:08] "POST /authenticate HTTP/1.1" 200 - 127.0.0.1 - - [05/Oct/2023 12:15:08] "GET /fanbase HTTP/1.1" 401 -
成功请求日志
Token in incoming request: Bearer eyJhbGciOiJIUzI1DSMFNFDHN79878CI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY5NjUxODkwOCwianRpIjoidufghfuighsigudfggsdufgsihgItY2UxZDAyOWNmYTlmIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3RAZ21haWwuY29tIiwibmJmIjoxNjk2NTE4OTA4LCJleHAiOjE2OTY1MTk4MDh9.PC4TdX_XAgl91w1YQ8ZbDj98hfNgNZSHXsue0HphnVY 127.0.0.1 - - [05/Oct/2023 12:20:35] "GET /fanbase HTTP/1.1" 200 -
服务器端Flask代码
import os from flask import Flask, render_template, jsonify, request from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity # Initialize Flask app app = Flask(__name__) app.config['SECRET_KEY'] = 'supersecretkey' app.config['JWT_SECRET_KEY'] = 'your-jwt-secret' # replace this with your JWT secret key app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///your_database.db' # Import after app is initialized from models import db, User, Role # Initialize Database db.init_app(app) # This is important with app.app_context(): db.create_all() # Initialize JWT Manager jwt = JWTManager(app) @app.route('/') def home(): image_files = [f for f in os.listdir('static/images/') if f.endswith('.jpeg')] return render_template('index.html', image_files=image_files) @app.route('/login', methods=['GET']) def login(): return render_template('login.html') @app.route('/verify_token', methods=['POST']) @jwt_required() def verify_token(): auth_header = request.headers.get('Authorization') print(f"Received auth header: {auth_header}") headers = dict(request.headers) print("Headers in verify_token:", headers) # Debug print identity = get_jwt_identity() # Get identity from token print("Identity:", identity) # Debug print return jsonify({"message": "Token is valid"}), 200 @app.route('/authenticate', methods=['POST']) def authenticate(): data = request.json username = data.get('username') password = data.get('password') user = User.query.filter_by(username=username).first() # If the user exists, verify the password if user: if user.password == password: access_token = create_access_token(identity=username) # instead of identity={'username': username} return jsonify(access_token=access_token), 200 elif not user: # Create a new user if not found new_user = User(username=username, password=password) db.session.add(new_user) db.session.commit() # Issue an access token for the new user access_token = create_access_token(identity=username) return jsonify(access_token=access_token), 200 else: return jsonify({"authenticated": False}), 401 @app.route('/fanbase', methods=['GET']) @jwt_required() def fanbase(): print("Token in incoming request:", request.headers.get("Authorization")) username = get_jwt_identity() return render_template('fanbase.html', username=username) if __name__ == '__main__': app.run(debug=True)
前端index.html代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My Music Project</title> <link rel="stylesheet" type="text/css" href="/static/css/style.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script type="module"> import jwtDecode from 'https://cdn.jsdelivr.net/npm/jwt-decode@3.1.2/+esm'; window.jwtDecode = jwtDecode; </script> <script> console.log('jwt_decode should be loaded now', window.jwt_decode); </script> <script src="/static/js/p5.min.js"></script> <script src="/static/js/sketch.js" defer></script> <script src="/static/js/script.js" defer></script> <script type="text/javascript"> document.addEventListener("DOMContentLoaded", function() { try { var image_files = {{ image_files|tojson }}; var random_index = Math.floor(Math.random() * image_files.length); var random_image = "/static/images/" + image_files[random_index]; document.getElementById("main-container").style.backgroundImage = "url('" + random_image + "')"; } catch (error) { console.error("Error occurred:", error); } }); </script> </head> <body> <div id="p5-container"></div> <div id="main-container"> <!-- Image will be set as the background --> <div id="social-icons"> <a href="https://www.facebook.com/yourpage"><i class="fab fa-facebook-f"></i></a> <a href="https://www.instagram.com/super.condutores"><i class="fab fa-instagram"></i></a> <a href="https://spotify.link/fJMjUIapADb"><i class="fab fa-spotify"></i></a> <a href="https://www.youtube.com/super.condutores"><i class="fab fa-youtube"></i></a> <a href="https://www.discord.com/super.condutores"><i class="fab fa-discord"></i></a> </div> <nav id="main-nav"> <ul> <li><a href="#musica">Música</a></li> <li><a href="#videos">Vídeos</a></li> <li><a href="#fotos">Fotos</a></li> <li><a href="#shows">Shows</a></li> <li><a href="#fanbase">Fanbase</a></li> </ul> </nav> </div> <script> $(document).ready(function() { $("li > a[href='#fanbase']").on("click", function(e) { e.preventDefault(); var token = localStorage.getItem('access_token'); console.log("Retrieved Token:", token); if (!token) { console.log('No token found, redirecting to login'); window.location.href = "/login"; return; } var headers = {'Authorization': 'Bearer ' + token}; console.log("Headers:", headers); $.ajax({ beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + token); }, url: '/fanbase', method: 'GET', success: function(response) { console.log('Successfully fetched fanbase', response); }, error: function() { console.log('Failed to fetch fanbase'); } }); }); }); </script> </body> </html>
前端login.html代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> <link rel="stylesheet" type="text/css" href="/static/css/style.css"> <script type="text/javascript"> document.addEventListener("DOMContentLoaded", function() { var image_files = ['website.002.jpeg', 'website.012.jpeg', 'website.014.jpeg','website.018.jpeg','website.060.jpeg','website.062.jpeg','website.068.jpeg']; var random_index = Math.floor(Math.random() * image_files.length); var random_image = "/static/images/" + image_files[random_index]; document.body.style.backgroundImage = "url('" + random_image + "')"; }); </script> </head> <body class="login-page"> <div class="form-container"> <form id="login-form"> <label for="username">Username:</label> <input type="text" id="username" name="username" required><br> <label for="password">Password:</label> <input type="password" id="password" name="password" required><br> <input type="submit" value="Login"> </form> </div> <script> document.addEventListener("DOMContentLoaded", function() { var form = document.getElementById('login-form'); form.addEventListener('submit', function(event) { event.preventDefault(); var username = document.getElementById('username').value; var password = document.getElementById('password').value; fetch('/authenticate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: password }) }) .then(response => { if (response.status === 200) { return response.json(); } else { throw new Error('Failed to authenticate'); } }) .then(data => { if (data.access_token) { localStorage.setItem('access_token', data.access_token); window.location.href = '/fanbase'; // Comment this out console.log('Token stored but not redirecting'); // Add this line for debug } else { alert('Authentication failed'); } }) .catch(error => { console.log('Error:', error); alert('Authentication failed'); }); }); }); </script> </body> </html>
问题原因及解决方案
核心问题
登录成功后直接通过window.location.href = '/fanbase'跳转,此时浏览器发起的是普通GET请求,不会自动携带Authorization Header,导致服务器返回401。而刷新首页后点击访问,是通过index.html中的AJAX请求手动设置了Header,所以成功。
解决方案
方案1:修改登录后跳转逻辑
登录成功后跳转到首页,让用户手动点击Fanbase链接触发AJAX请求:
// login.html中修改跳转代码 .then(data => { if (data.access_token) { localStorage.setItem('access_token', data.access_token); window.location.href = '/'; // 跳转到首页而非直接访问fanbase console.log('Token stored, redirecting to home'); } else { alert('Authentication failed'); } })
方案2:直接跳转时手动携带Token
如果需要登录后直接进入fanbase页面,可通过AJAX请求获取页面内容并替换当前页面:
// login.html中修改登录成功后的逻辑 .then(data => { if (data.access_token) { localStorage.setItem('access_token', data.access_token); // 用AJAX请求fanbase页面并替换内容 fetch('/fanbase', { headers: { 'Authorization': 'Bearer ' + data.access_token } }) .then(res => res.text()) .then(html => { document.body.innerHTML = html; }); } else { alert('Authentication failed'); } })
方案3:启用Cookie存储Token(推荐)
在服务器端配置Flask-JWT-Extended使用Cookie存储Token,浏览器会自动在请求中携带Cookie,无需手动设置Header:
# 服务器端添加配置 app.config['JWT_TOKEN_LOCATION'] = ['cookies'] app.config['JWT_COOKIE_SECURE'] = False # 开发环境设为False,生产环境需配合HTTPS改为True app.config['JWT_COOKIE_CSRF_PROTECT'] = False # 不需要CSRF保护可关闭
额外建议
- 禁止明文存储密码:当前代码中用户密码为明文存储,需使用
werkzeug.security的generate_password_hash和check_password_hash进行加密。 - 处理Token失效场景:在AJAX请求的error回调中清除无效Token并跳转至登录页。
内容的提问来源于stack exchange,提问作者8-Bit Borges
相关产品推荐
相关产品推荐

