如何解决Heroku上Django后端对接Flutter前端的CORS问题?
当前环境
- 后端:部署在Heroku的Django
- 前端:部署在
https://anirni.web.app的Flutter应用,本地测试地址为http://localhost:52360 - CORS处理库:使用
django-cors-headers
settings.py相关配置
""" Django settings for anirni_server project. Generated by 'django-admin startproject' using Django 5.1. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os from pathlib import Path import dj_database_url from corsheaders.defaults import default_headers EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_HOST = 'mail.anirni-dz.com' # Remplacez par le serveur SMTP de Bluehost EMAIL_PORT = 465 # Port SMTP (généralement 587 pour TLS) EMAIL_USE_TLS = False EMAIL_USE_SSL = True # Activer SSL EMAIL_HOST_USER = '###' # Adresse email EMAIL_HOST_PASSWORD = '###' # Mot de passe DEFAULT_FROM_EMAIL = '###' # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent from corsheaders.defaults import default_methods # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '####' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', # Assurez-vous qu'elle est avant les autres apps comme 'auth' 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', "django_filters", 'corsheaders', 'rest_framework_simplejwt.token_blacklist', # Pour la gestion des tokens JWT "django_crontab", 'basic_rest', ] CORS_ALLOW_METHODS = list(default_methods) + ["PATCH"] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework.authentication.SessionAuthentication', # Optionnel pour l'authentification par session ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20 # Nombre de résultats par page } MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', ] MIDDLEWARE.insert(0, 'basic_rest.middleware.LogHeadersMiddleware') CORS_ALLOWED_ORIGINS = [ "https://anirni.web.app", # Domaine de votre application Flutter "http://localhost:52360", # Pour le développement local ] ALLOWED_HOSTS = ['anirni.web.app', 'anirnirest-f97872c47bed.herokuapp.com'] CORS_ALLOW_CREDENTIALS = True # Autoriser les cookies/credentials CORS_ALLOW_HEADERS = list(default_headers) + [ "authorization", "x-requested-with", ] SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True ROOT_URLCONF = 'anirni_server.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'anirni_server.wsgi.application' # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.getenv('DB_NAME',"###"), 'USER': os.getenv('DB_USER',"###"), 'PASSWORD': os.getenv('DB_PASSWORD',"####"), 'HOST': os.getenv('DB_HOST',"###"), 'PORT': os.getenv('DB_PORT',"5432"), } } DATABASES = { 'default': dj_database_url.config(conn_max_age=600, ssl_require=True) } # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/5.1/howto/static-files/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = 'static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'INFO', # Niveau minimal de log (DEBUG, INFO, WARNING, ERROR, CRITICAL) }, '__main__': { # Logger pour le middleware personnalisé 'handlers': ['console'], 'level': 'INFO', }, }, } # Configurer la durée du token from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'AUTH_HEADER_TYPES': ('Bearer',), }
已尝试操作
- 临时设置
CORS_ALLOW_ALL_ORIGINS = True,问题依旧存在 - 确认HTTPS已启用,Heroku已正确将HTTP重定向到HTTPS
- 使用curl和Postman测试API,响应正常,但Flutter端请求被CORS策略拦截
预期行为
Flutter前端能够正常向Django后端发送请求(包括OPTIONS预检请求),不会被CORS拦截。
错误信息
Flutter及浏览器控制台报错:
Access to XMLHttpRequest at 'https://anirnirest-f97872c47bed.herokuapp.com/get_user_info' from origin 'https://anirni.web.app' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
额外细节
- API使用Django REST Framework,采用JWT Token认证
- 已确认所有API端点独立测试正常
- 获取Token的API和公共API可以正常访问,但所有需要JWT认证的API均被CORS拦截
解决方案
1. 调整Middleware顺序
CorsMiddleware必须放在CommonMiddleware之前才能正确处理CORS响应头,修改后的MIDDLEWARE列表:
MIDDLEWARE = [ 'basic_rest.middleware.LogHeadersMiddleware', # 自定义中间件保持在最前 'corsheaders.middleware.CorsMiddleware', # 移至此处,CommonMiddleware之前 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', ]
2. 排查自定义中间件干扰
检查basic_rest.middleware.LogHeadersMiddleware是否修改或删除了响应头。如果该中间件操作了响应对象,需确保它不会清除Access-Control-Allow-Origin等CORS相关头。
3. 强制认证失败时返回CORS头
需要认证的API如果JWT无效,Django会返回401,但此时CorsMiddleware可能还未添加响应头。添加全局异常处理来确保即使认证失败也返回正确的CORS头:
在basic_rest应用中创建exception_handler.py:
from rest_framework.views import exception_handler def custom_exception_handler(exc, context): response = exception_handler(exc, context) if response is None: return response # 添加强制CORS头 response['Access-Control-Allow-Origin'] = "https://anirni.web.app" response['Access-Control-Allow-Credentials'] = "true" return response
在settings.py的REST_FRAMEWORK中配置:
REST_FRAMEWORK = { # ... 其他原有配置 'EXCEPTION_HANDLER': 'basic_rest.exception_handler.custom_exception_handler', }
4. 重新部署验证
修改配置后重新部署到Heroku,用浏览器开发者工具查看请求响应头,确认Access-Control-Allow-Origin是否正确返回。
内容的提问来源于stack exchange,提问作者Oussama Saidi

