Flutter+Django JWT认证:登出返回403 Forbidden问题排查求助
问题排查:Flutter + Django 登出403 Forbidden错误
登录功能正常,但执行登出操作时返回403 Forbidden,提示用户未认证,怀疑和Flutter的JWT请求头设置有关。
相关代码
Flutter 认证服务代码
class AuthService { // Base URL for the user endpoints final String _loginUrl = '$BASE_URL/users/login'; final String _isLoggedInUrl = '$BASE_URL/users/is_logged_in'; final String _logoutUrl = '$BASE_URL/users/logout'; Future<String?> login(String email, String password) async { final response = await http.post( Uri.parse(_loginUrl), headers: {'Content-Type': 'application/json'}, body: json.encode({'email': email, 'password': password}), ); if (response.statusCode == 200) { // Store the JWT token in shared preferences final prefs = await SharedPreferences.getInstance(); final token = json.decode(response.body)['jwt']; await prefs.setString('jwt', token); return token; } else { throw Exception('Failed to login'); } } Future<bool> isLoggedIn() async { print("In isLoggedIn"); final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('jwt'); // If token is null, the user is not logged in if (token == null) { return false; } final response = await http.post( Uri.parse(_isLoggedInUrl), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token', }, ); // Check response status and print for debugging if (response.statusCode == 200) { final sth = json.decode(response.body); print(sth); return true; // User is logged in if the response is successful } else { return false; // User is not logged in if the response fails } } Future<void> logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('jwt'); // Remove JWT from shared preferences // Optionally call the logout API final token = prefs.getString('jwt'); // Get the token for logout request await http.post( Uri.parse(_logoutUrl), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', // Include token if required }, ); } }
Django 后端代码
from datetime import datetime, timedelta, timezone from django.shortcuts import get_object_or_404 import jwt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import IsAuthenticated from users.models import User from .serializers import UserSerializer from .authentication import JWTAuthentication # Helper function to generate JWT tokens def generate_jwt(user): print("Generating JWT token for user:", user.email) payload = { 'id': user.id, 'exp': datetime.now(timezone.utc) + timedelta(minutes=60), 'iat': datetime.now(timezone.utc) } token = jwt.encode(payload, 'secret', algorithm='HS256') return token # View for user registration class RegisterView(APIView): def post(self, request): print("Inside RegisterView: Registering user") serializer = UserSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() # Generate JWT token for the new user token = generate_jwt(user) response = Response( {'status': 'success', 'jwt': token}, status=201 ) response.set_cookie(key='jwt', value=token, httponly=True) print("User registered successfully and JWT token set in cookie") return response # View for user login class LoginView(APIView): def post(self, request): print("Inside LoginView: Logging in user") email = request.data.get('email') password = request.data.get('password') user = get_object_or_404(User, email=email) if not user.check_password(password): print("Incorrect password for user:", email) raise AuthenticationFailed('Incorrect password!') token = generate_jwt(user) response = Response( {'status': 'success', 'jwt': token}, status=200 ) response.set_cookie(key='jwt', value=token, httponly=True) print("User logged in successfully and JWT token set in cookie") return response # View to get authenticated user details class UserView(APIView): authentication_classes = [JWTAuthentication] def get(self, request): print("Inside UserView: Fetching user details") user = request.user serializer = UserSerializer(user) return Response(serializer.data, status=200) # View for user logout class LogoutView(APIView): authentication_classes = [JWTAuthentication] def post(self, request): print("Inside LogoutView: Logging out user") response = Response({'message': 'success'}, status=200) response.delete_cookie('jwt') print("User logged out successfully and JWT cookie deleted") return response # New View to check if the user is logged in class IsLoggedInView(APIView): authentication_classes = [JWTAuthentication] def get(self, request): print("Inside IsLoggedInView: Checking login status") user = request.user token = generate_jwt(user) return Response({'is_logged_in': True, 'jwt': token}, status=200)
错误日志
User logged in successfully and JWT token set in cookie [28/Oct/2024 13:48:24] "POST /users/login HTTP/1.1" 200 166 Forbidden: /users/logout [28/Oct/2024 13:48:39] "POST /users/logout HTTP/1.1" 403 29
问题原因及修复方案
核心问题:Flutter登出方法中token获取顺序错误
在Flutter的logout方法里,先调用await prefs.remove('jwt');删除了本地存储的token,之后再去prefs.getString('jwt')获取token,此时拿到的必然是null。这导致请求头中的Authorization字段变成Bearer null,Django后端的JWT认证无法识别,直接返回403。
修复代码
调整token获取和删除的顺序,先拿到token再删除本地存储:
Future<void> logout() async { final prefs = await SharedPreferences.getInstance(); // 先获取token再删除 final token = prefs.getString('jwt'); await prefs.remove('jwt'); // Remove JWT from shared preferences // 调用登出API if (token != null) { // 确保token存在时才发送请求 await http.post( Uri.parse(_logoutUrl), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); } }
额外检查点
Django的JWTAuthentication类是否支持从请求头解析token
你的Django登录接口同时返回了响应体中的jwt和httponly cookie,但Flutter是通过Authorization头传递token的,需要确保JWTAuthentication类能正确从请求头的Bearer token中解析认证信息。如果认证类只从cookie中取token,那即使请求头传了也会认证失败。示例JWTAuthentication类实现参考(确保支持请求头解析):
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed import jwt from users.models import User class JWTAuthentication(BaseAuthentication): def authenticate(self, request): # 先从请求头获取token auth_header = request.headers.get('Authorization') if auth_header and auth_header.startswith('Bearer '): token = auth_header.split(' ')[1] else: # 再尝试从cookie获取 token = request.COOKIES.get('jwt') if not token: return None try: payload = jwt.decode(token, 'secret', algorithms=['HS256']) user = User.objects.get(id=payload['id']) return (user, None) except jwt.ExpiredSignatureError: raise AuthenticationFailed('Token expired') except jwt.InvalidTokenError: raise AuthenticationFailed('Invalid token') except User.DoesNotExist: raise AuthenticationFailed('User not found')登出接口的权限设置
确保LogoutView的authentication_classes配置正确,且没有额外的权限限制导致认证通过后仍被拒绝。
内容的提问来源于stack exchange,提问作者Punreach Rany
相关产品推荐
相关产品推荐

