You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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',
            },
        );
    }
}

额外检查点

  1. 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')
    
  2. 登出接口的权限设置
    确保LogoutView的authentication_classes配置正确,且没有额外的权限限制导致认证通过后仍被拒绝。

内容的提问来源于stack exchange,提问作者Punreach Rany

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:32:01