首页 / 资讯中心 / 文章详情

Django跨域报错corsheaders模块缺失排查与配置指南

Django跨域报错corsheaders模块缺失排查与配置指南 ★ FEATURED ARTICLE
上周三下午某开发者把一段 Django 接口联调代码丢进群里附了一张终端截图满屏红字里最扎眼的一行是ModuleNotFoundError: No module named corsheaders。他的原话是“我明明 pip install 过了”。这个场景我见过太多次前后端分离的项目一到联调阶段浏览器控制台开始刷 CORS 报错后端第一反应是找django-cors-headers装完、加进INSTALLED_APPS、重启服务然后一头撞在这个 ImportError 上。问题本身不复杂但它牵扯出来的东西不少虚拟环境、解释器路径、包名与模块名的错位、中间件顺序、3.x 到 4.x 的配置项改名、白名单策略、预检请求。这篇文章就把这条链路从头到尾拆一遍从“为什么这一行报错会出现在这里”讲到“配好之后怎么保证它真的在干活”。适合刚接手中台接口的新手也适合那些被跨域问题反复折腾过、想一次性把配置逻辑理顺的老手。1. 先把报错读明白缺的不是跨域功能是一个名字对不上的包1.1 三种报错文本同一个病根corsheaders这个报错在不同 Python 版本下会长得不一样很多人被这个差异带偏了方向。Python 2 时代统一抛ImportError: No module named corsheadersPython 3.6 之后ModuleNotFoundError从ImportError里派生出来专门表示“模块找不到”所以新版本会看到ModuleNotFoundError: No module named corsheaders。两者是父子类关系except ImportError都能接住本质完全一致解释器在sys.path列出的所有目录里翻了一遍没找到名为corsheaders的包目录。需要特别指出的是这个报错出现的时机很有信息量。如果是启动 Django 时立刻崩报错栈顶大概率是django.core.management或者django.utils.module_loading说明问题出在INSTALLED_APPS里写了corsheaders但包没装到当前解释器如果是请求进来才报栈顶出现在某个视图文件里那多半是你自己写了from corsheaders.xxx import ...属于代码里显式引用。前者是配置问题后者是代码问题排查路径不一样。还有一种容易混淆的情况报错信息里写的是No module named corsheaders.middleware或者No module named corsheaders.middleware.CorsMiddleware。这种说明corsheaders包本身找到了但包里没有middleware这个子模块。原因通常是装的不是官方那个包而是某个历史上存在过的同名包或者装了一个版本极老、目录结构已经和文档对不上的包。这两种错误的处理方式完全不同读报错时盯住“点号后面还有没有东西”这一步很关键。1.2 为什么安装名和导入名故意不一样这是新手最容易卡住的地方也是这条报错最核心的病因。pip install django-cors-headers但代码里写的是import corsheaders。安装名带django-前缀、用连字符导入名不带前缀、全小写无分隔符。这个约定在 Django 生态里到处都是django-rest-framework装完导入rest_frameworkdjango-filter导入django_filtersdjango-debug-toolbar导入debug_toolbar。原因在于 PyPI 的包名规则和 Python 的标识符规则不是一回事PyPI 允许连字符、允许前缀做命名空间区分同一个作者可能同时维护 Django 版和 Flask 版而 Python 的模块名必须是合法标识符下划线可以有连字符绝对不行。理解了这一层你就能明白为什么“我装了 django-cors-headers为什么导入 corsheaders 报错”这个因果链在逻辑上其实是不成立的——除非装的不是那个包。反过来有人试图pip install corsheaders不带前缀PyPI 上确实存在过这个名字的包但它和官方维护的django-cors-headers不是同一个东西装上去要么报更奇怪的错要么装了个空壳。所以正确的动作永远是安装用django-cors-headers导入和配置用corsheaders这两者之间的转换由 pip 的包元数据完成不需要你手动干预。顺带提一句INSTALLED_APPS里的写法。这里必须写corsheaders不能写django-cors-headers也不能写corsheaders.apps.CorsHeadersConfig——虽然理论上 Django 支持 AppConfig 路径写法但这个包并没有提供独立的 AppConfig 类供你指定部分老版本有新版本已经简化。写错这一处报错信息会变成ModuleNotFoundError: No module named django-cors-headers连字符出现在模块名里一眼就能认出来是配置写错了。2. 按顺序排查五个检查点基本能锁定九成情况2.1 第一检查点解释器到底是谁绝大多数“我明明装了”的案子最后都归结到这一条上。你的机器上可能同时存在系统 Python、pyenv 管理的某个版本、conda 环境、虚拟环境.venv、IDE 自带的解释器。pip install走的是某一个python manage.py runserver走的是另一个两边井水不犯河水。确认方式很直接。先看当前 shell 里python指向哪里which python which pip python -c import sys; print(sys.executable) python -m pip --version前两条输出应该在同一个目录下比如都在~/.virtualenvs/myproj/bin/如果which pip指向/usr/local/bin/pip而which python指向虚拟环境那基本可以确诊。python -m pip --version这条更重要它强制用当前解释器去调 pip输出的路径和sys.executable一致才说明两者匹配。我个人的习惯是彻底抛弃裸pip命令一律用python -m pip install xxx。多敲几个字符省掉无数排查时间。虚拟环境激活之后也建议再跑一次python -c import sys; print(sys.prefix)确认sys.prefix指向的是虚拟环境根目录而不是/usr。有些 shell 配置会在激活虚拟环境后仍然保留旧的 PATH 顺序导致pip被覆盖这种情况在 Windows 的 Git Bash 里尤其常见。注意用 conda 的时候conda install和pip install装到的是同一个 site-packages但两者的依赖解析器互相不知道对方做了什么。混用久了容易出现“pip 说已安装、import 却找不到”的诡异现象。Django 生态的包建议统一走 pip。2.2 第二检查点包有没有真的落到这个解释器里路径对了之后第二步是确认包是否存在于当前环境的 site-packages 中。三条命令配合着看python -m pip show django-cors-headers python -m pip list | grep -i cors python -c import corsheaders; print(corsheaders.__file__)第一条会输出包的版本、安装位置、依赖信息。如果输出WARNING: Package(s) not found那就是真没装到这儿。第二条做模糊匹配能顺带发现有没有装过corsheaders这个同名包如果有两条输出立刻卸掉非官方那个。第三条是最终验证能 import 成功并打印出.py文件路径说明链路彻底通了如果还报错把sys.path打出来看看python -c import sys; [print(p) for p in sys.path]输出的列表里应该包含当前环境的site-packages目录。如果这个目录不在列表里问题就比“包没装”更严重了——可能是PYTHONPATH被污染或者虚拟环境本身损坏重建一个环境比修更省事。还有一个隐蔽情况包确实装了但装到了用户级目录~/.local/lib/python3.x/site-packages而当前虚拟环境设置了include-system-site-packages false默认就是 false所以看不见。pip install --user是罪魁祸首在虚拟环境里用这个参数毫无意义纯属自找麻烦。2.3 第三检查点拼写、下划线与单复数这一条听起来很傻但实际发生的频率超乎想象。常见的错误拼写包括corsheader少 s、corsHeaders大小写、cors_headers加下划线、crossheaders多一个 s。Python 的模块名区分大小写在 macOS 和 Windows 的默认文件系统上可能不区分HFS 和 NTFS 默认大小写不敏感导致本地能跑、部署到 Linux 就崩。这类问题最难查因为本地完全复现不了。我的做法是把INSTALLED_APPS里和跨域相关的这一行单独拎出来用注释标上对应的 pip 包名形成一种自我文档化INSTALLED_APPS [ # ... corsheaders, # pip install django-cors-headers # ... ]看起来啰嗦但接手项目的人包括三个月后的自己会感谢这一行注释。同样的注释方式也适用于MIDDLEWARE里的corsheaders.middleware.CorsMiddleware。2.4 第四检查点requirements 与镜像源带来的假安装团队协作里常见的翻车现场requirements.txt里写的是django-cors-headers但某个成员本地用的是pip install -r requirements.txt而他的 pip 配置了国内镜像源镜像同步延迟导致拿到的版本偏旧或者更糟requirements.txt里写错了包名比如写成django-cors-header少个 spip 报错但被|| true之类的脚本吞掉了环境看起来装完了实际缺这个包。排查方式是先看requirements.txt原文再比对pip freeze的输出grep -i cors requirements.txt python -m pip freeze | grep -i cors两边应该对得上。如果freeze里没有说明构建过程某一步失败了。CI 环境里这种情况尤其要注意Dockerfile 里多阶段构建时依赖安装阶段和运行阶段如果不是同一个基础镜像、同一个 Python 版本装好的包可能根本不在最终镜像的sys.path里。2.5 第五检查点同名包和旧缓存PyPI 上曾存在过名为corsheaders的包功能与官方包不同装上之后import corsheaders可能成功但缺少middleware子模块。判断方法是看pip show输出的包名官方的是django-cors-headers作者信息、主页链接都能对上野路子包的名字就叫corsheaders。发现装错了先卸干净python -m pip uninstall corsheaders django-cors-headers -y python -m pip install django-cors-headers4.0另一个来源是 pip 的本地缓存。极少数情况下缓存里的 wheel 文件损坏安装过程“成功”但解压出来的目录不完整。清缓存重装python -m pip cache purge python -m pip install --no-cache-dir django-cors-headers--no-cache-dir这个参数在排查疑难杂症时非常好用代价只是多花几秒下载时间。3. 装对之后才刚开始INSTALLED_APPS 与 MIDDLEWARE 的接线顺序3.1 一份可以直接抄的最小配置ImportError 解决掉之后很多人会以为万事大吉结果浏览器控制台照样刷红。因为包能导入只是第一步它得被 Django 真正加载并挂到请求处理链上。最小可用配置分三块缺一不可。第一块是settings.py里的INSTALLED_APPS加在列表靠前的位置即可它本身不涉及加载顺序INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, corsheaders, # 新增 rest_framework, myapp, ]第二块是MIDDLEWARE顺序在这里是有强约束的MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 尽量靠前 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, ]第三块是策略配置开发阶段可以先放开生产环境必须收紧这块在第 5 节细讲。3.2 中间件位置放错的两种典型症状CorsMiddleware的位置规则在官方文档里写得很明确放在CommonMiddleware之前如果用了SessionMiddleware又需要读 session放在它之后。原因在于 Django 中间件的执行模型——请求阶段从上往下走响应阶段从下往上回。CorsMiddleware需要在请求阶段尽早判断来源是否合法并给响应加上Access-Control-Allow-Origin等头如果它排在CommonMiddleware后面CommonMiddleware可能已经生成了 404 或者重定向响应此时 CORS 头来不及加浏览器端看到的就是“跨域被拦”但服务端日志里一切正常。症状一简单 GET 请求正常带自定义头的 POST 请求在浏览器里报跨域但 curl 或者 Postman 直接请求完全正常。这通常是因为预检请求OPTIONS被前面的中间件拦截了或者响应头没加上。排查方法是打开浏览器开发者工具的 Network 面板找到那条 OPTIONS 请求看响应头里有没有Access-Control-Allow-Origin。没有就是中间件没生效。症状二所有响应头里出现了两条Access-Control-Allow-Origin内容一样但重复。浏览器对重复的 CORS 头会直接判定失败。这种情况往往是两边都加了头——比如 Nginx 配置里写了add_header Access-Control-Allow-Origin *;Django 这边也加了。解决办法是只保留一层推荐在 Django 里管Nginx 那边去掉因为白名单逻辑在应用层更好维护。提示中间件顺序错了不会抛异常只会静默失效。改完MIDDLEWARE之后一定要重启服务Django 的自动重载在settings.py大幅改动时偶尔会漏掉。4. 版本差异是第二颗雷3.x 到 4.x 的配置项改名4.1 配置项新旧对照跨过 ImportError 这道坎下一个坑往往在升级之后才踩到。django-cors-headers从 3.5 开始陆续重命名配置项4.0 版本直接把旧名字移除。如果你从某个老项目里拷贝了配置过来或者在搜索引擎里翻到一篇 2019 年的文章照着写大概率不生效。旧配置项3.x 起废弃新配置项4.x 必须用说明CORS_ORIGIN_ALLOW_ALLCORS_ALLOW_ALL_ORIGINS布尔值放开所有来源CORS_ORIGIN_WHITELISTCORS_ALLOWED_ORIGINS明确列出的来源列表CORS_ORIGIN_REGEX_WHITELISTCORS_ALLOWED_ORIGIN_REGEXES正则匹配的来源CORS_ALLOW_METHODS不变允许的 HTTP 方法CORS_ALLOW_HEADERS不变允许的请求头CORS_EXPOSE_HEADERS不变暴露给前端 JS 的响应头CORS_ALLOW_CREDENTIALS不变是否允许携带 CookieCORS_PREFLIGHT_MAX_AGE不变预检结果缓存秒数改名之后最直接的表现是配置写了但完全不生效跨域照旧被拦。因为新版本根本不认识这些 keyDjango 的 settings 是普通模块属性写错了不会有任何警告静默忽略。我见过一个项目在 4.2 上跑着CORS_ORIGIN_WHITELIST前端一直连不上查了两天才发现是版本对不上。4.2 升级顺序与兼容写法如果项目还卡在老版本比如 3.4升级时要按这个顺序走先升到 3.5 或 3.7 这个区间此时新旧配置项都认把settings.py里的旧 key 全部替换成新 key跑一遍测试确认功能正常再升到 4.x把旧 key 的残留删干净。跳跃式升级容易出问题因为中间版本的兼容期被跳过了。兼容期可以写一段临时判断让代码在新旧版本下都能跑import corsheaders CORS_VERSION tuple(int(x) for x in corsheaders.__version__.split(.)[:2]) if CORS_VERSION (3, 5): CORS_ALLOWED_ORIGINS [https://frontend.example.internal] CORS_ALLOW_ALL_ORIGINS False else: CORS_ORIGIN_WHITELIST [https://frontend.example.internal] CORS_ORIGIN_ALLOW_ALL False这段代码不优雅但迁移期能救命。迁完之后务必删掉别让它长期留在代码库里。另外corsheaders.__version__这个属性在极老版本上可能不存在用importlib.metadata.version(django-cors-headers)更稳妥。版本升级还带出另一个变化4.x 对CORS_ALLOWED_ORIGINS的校验更严格了。以前写example.com不带协议会被静默接受并导致匹配失败现在某些版本会在启动时直接抛ImproperlyConfigured提示来源格式不对。这个改动其实是好事把配置错误从运行时前移到启动时但如果你是从老版本升上来的第一次启动就可能被这个异常拦住心理上要有准备。5. 跨域策略的取舍白名单、凭据、预检请求5.1 开发环境放开生产环境收紧开发阶段图省事很多人直接CORS_ALLOW_ALL_ORIGINS True。本地跑没问题但如果这个值随着配置文件一起被带到生产环境等于对所有站点开放了你的接口。浏览器层面看任何网站都能用用户的 Cookie 调你的接口——虽然有credentials的限制兜底但风险依然实打实存在。比较稳妥的做法是用环境变量区分import os DEBUG os.environ.get(DJANGO_DEBUG, 0) 1 if DEBUG: CORS_ALLOW_ALL_ORIGINS True else: CORS_ALLOW_ALL_ORIGINS False CORS_ALLOWED_ORIGINS [ https://app.example.internal, https://admin.example.internal, ] CORS_ALLOWED_ORIGIN_REGEXES [ r^https://preview-[a-z0-9]\.example\.internal$, ]正则那条是给预览环境用的每次提 PR 会生成一个带随机后缀的域名硬列白名单维护不过来。不过正则要写得收敛一点^https://preview-这种前缀匹配容易被绕过攻击者注册preview-evil.example.internal的子域就能命中。用example.internal这种内部域名能规避掉大部分这类问题。5.2 带 Cookie 的请求为什么不能配成通配符这是 CORS 规范里最容易被误解的一条。当请求携带凭据Cookie、HTTP 认证信息、TLS 客户端证书时响应头里的Access-Control-Allow-Origin必须是具体的来源不能是*Access-Control-Allow-Credentials必须是true。这条规则由浏览器强制执行服务端配错了浏览器就直接拒。对应的 Django 配置是CORS_ALLOW_CREDENTIALS True CORS_ALLOWED_ORIGINS [https://app.example.internal] # 注意此时 CORS_ALLOW_ALL_ORIGINS 必须为 False如果同时设了CORS_ALLOW_ALL_ORIGINS True和CORS_ALLOW_CREDENTIALS True新版库会在启动时抛异常老版本则会静默地把来源反射成请求的 Origin看起来能用实际上等于放开了所有来源带 Cookie 访问极其危险。前端这边也有对应要求用 fetch 时必须显式写credentials: include用 axios 要写withCredentials: true。少了这一项浏览器不发 Cookie后端拿不到 session接口返回 401然后一堆人开始怀疑是跨域配置没生效其实配置是对的只是前端没带凭据。5.3 OPTIONS 预检 405 与自定义请求头浏览器判断一个请求是否触发预检取决于请求方法、Content-Type 和自定义头。application/json的 POST、带Authorization头的请求、PUT/DELETE 方法全都会先发一个 OPTIONS 请求探路。这个 OPTIONS 请求不带业务数据只带Access-Control-Request-Method和Access-Control-Request-Headers两个头服务端需要在响应里明确告诉浏览器“允许用什么方法、允许带什么头”。如果前端用了自定义头比如X-Request-Id、X-Tenant-Key而CORS_ALLOW_HEADERS里没列出来预检响应里就不会包含它浏览器判定失败。默认的CORS_ALLOW_HEADERS覆盖了一批常见头但业务自定义的必须手动加from corsheaders.defaults import default_headers CORS_ALLOW_HEADERS ( *default_headers, x-request-id, x-tenant-key, )default_headers是个元组解包进去能保证基础头不丢。这个写法比手动罗列content-type、authorization这些更安全因为库升级时默认头会跟着变。OPTIONS 请求返回 405 是另一个高频问题。原因通常是路由层没有为这个路径注册 OPTIONS 方法或者某个自定义中间件在CorsMiddleware之前就把 OPTIONS 请求拦掉了。DRF 的APIView会处理 OPTIONS但如果用的是普通 Django 视图配require_http_methods([GET, POST])OPTIONS 会直接被拒。解决方式是把CorsMiddleware往前提让它在路由匹配之前就返回预检响应——这正是 3.2 节强调顺序的原因。6. 排查速查表与几条踩出来的经验6.1 症状到原因的对照表把前面几节的内容压缩成一张表遇到问题时按症状对号入座能省掉大半时间。现象最可能的原因首选动作启动即报No module named corsheaders包没装到当前解释器python -m pip show django-cors-headers装了但 import 仍失败虚拟环境未激活或 IDE 解释器选错python -c import sys; print(sys.executable)报No module named corsheaders.middleware装的是同名野包pip uninstall corsheaders后重装官方包配置了CORS_ORIGIN_WHITELIST但不生效4.x 已移除旧 key换成CORS_ALLOWED_ORIGINS简单请求正常POST 预检失败中间件顺序错OPTIONS 被提前拦截把CorsMiddleware前移到CommonMiddleware之前响应头重复两条Access-Control-Allow-OriginNginx 与 Django 都加了头去掉 Nginx 那侧的add_header前端带 Cookie 请求被拒前端没设withCredentials或后端没开CORS_ALLOW_CREDENTIALS两边同时检查自定义头触发跨域失败CORS_ALLOW_HEADERS未包含该头用*default_headers解包后追加预览环境跨域时好时坏白名单硬编码未覆盖动态域名改用CORS_ALLOWED_ORIGIN_REGEXES本地正常容器里报错构建阶段与运行阶段镜像不一致检查最终镜像的site-packages6.2 几条不太写在文档里的建议第一条把跨域配置单独抽一个模块。settings.py越写越长之后CORS_*这些配置散落在几百行里很难找。我一般的做法是建settings/cors.py里面只放跨域相关的开关和列表主配置里from .cors import *。好处有两层排查时一眼定位团队里谁改了白名单 git diff 也清爽。第二条本地开发优先用前端代理而不是放开后端跨域。Vite、webpack dev server 都支持把/api转发到后端这样浏览器看到的是同源请求压根不触发 CORS。CORS 配置只在测试环境和生产环境用真域名时才需要。这样做的好处是开发阶段可以完全绕开跨域问题把配置错误的暴露点推迟到更接近真实部署的环节反而更容易发现真问题。第三条白名单里别写localhost。有些人为了本地调试方便把http://localhost:3000写进生产配置。这个域名在攻击者机器上同样存在用户在自己电脑上访问恶意页面时页面可以起一个本地服务监听 3000 端口配合 DNS 重绑定做出一些意想不到的事。开发和生产用两套配置靠环境变量切别图省事合并。第四条跨域和 CSRF 是两回事别混着调。CORS 是浏览器限制跨源读取响应CSRF 是服务端防止跨站伪造请求。Django 的CsrfViewMiddleware会因为缺少 CSRF token 拒绝 POST这在跨域场景下表现为 403 而不是 CORS 报错。如果前端是 SPA 且用 session 认证要么在请求头里带X-CSRFToken要么改用 token 认证比如 JWT绕开 CSRF。我见过不少项目把csrf_exempt加到所有接口上“解决”跨域问题这属于用错误的手段掩盖错误的症状安全上留了窟窿。第五条加一个冒烟测试。在 CI 里跑一条 curl检查 OPTIONS 请求的响应头curl -s -o /dev/null -D - -X OPTIONS \ -H Origin: https://app.example.internal \ -H Access-Control-Request-Method: POST \ https://api.example.internal/api/v1/orders/ \ | grep -i access-control-allow-origin有输出就说明链路是通的。这条检查只要几秒钟但能在配置改动后第一时间拦住回归问题比等到联调时被前端追着问要高效得多。最后分享一个排查手法。当所有检查都做完还是不通时把CorsMiddleware的日志级别临时调到 DEBUGDjango 会在日志里打印每个请求的来源判断结果和最终附加的响应头。看到实际计算出的值比对着配置文档猜要快得多。调完记得改回去DEBUG 级别的日志量在生产环境是灾难。
阅读完成 · 觉得有帮助?
咨询建站