
1. 从一次线上事故说起Django接口被前端“拒绝访问”先讲个真实案例。上个月我维护的一个Django项目上线后前端同事火急火燎来找我说登录接口在测试环境跑得好好的一上生产就报错浏览器控制台红彤彤一串英文截图发过来关键信息就两条Access-Control-Allow-Origin缺失请求状态码直接是CORS error。我说这不是后端逻辑挂了这是跨域问题前端页面部署在A域名接口在B域名浏览器把请求拦了。很多刚接触Django的朋友一听“跨域”就头大觉得是特别玄乎的东西。其实用大白话讲就是浏览器出于安全考虑规定了一个页面里的脚本只能访问同源协议域名端口都相同的资源。如果前端页面在http://localhost:8080后端接口在http://localhost:8000这俩端口不一样就是“不同源”浏览器就会在发起真正的请求前先搞一个“预检”OPTIONS请求问后端“你允许我跨域访问吗”后端要是没配置允许规则浏览器直接帮前端把请求掐断。你可能会想后端明明写了接口浏览器凭什么多管闲事因为如果没有任何限制你在浏览器的某个网页里打开的恶意脚本就能随便往你的银行接口发请求你的Cookie还会被自动带上那后果不堪设想。所以这个“跨域限制”是浏览器安全模型的基石不能绕过只能配合。这篇文章我主要想跟你聊明白三件事跨域问题到底卡在哪个环节、Django后端怎么正确配置跨域、以及那些真正让你在线上踩坑的细节。这篇文章适合正在用Django做前后端分离项目的开发者不管你是刚写第一个接口的新手还是已经部署过几个项目但被CORS折磨过的老手都能从中找到你需要的答案。2. 为什么你的Django接口会触发跨域先理解同源策略和CORS机制2.1 同源策略浏览器里的“户籍管理制度”先说基础概念因为很多排查思路都建立在这上面。浏览器的“同源策略”Same-Origin Policy是一个历史悠久的规则页面里加载的脚本只能读取和操作“同源”的DOM、Cookie、Storage以及发起同源的网络请求。那什么叫“同源”三个条件必须完全一致协议http/https、域名example.com、端口8080/8000。只要有一个不一样就算“跨域”。做一个表格最直观前端页面地址后端接口地址是否同源原因http://localhost:8080http://localhost:8000否端口不同https://a.comhttp://a.com否协议不同http://a.comhttp://b.com否域名不同http://a.comhttp://a.com/api是只是路径不同不算跨域这里有个新手容易误会的点http://a.com和http://a.com:80其实是同源的因为80是http默认端口浏览器会自动补全。但如果你端口写成8080那就是另一回事了。2.2 CORS 是干嘛的给浏览器递话的“跨域通行证”既然浏览器默认不让跨域那正经的跨域需求比如前后端分离项目怎么解决答案就是CORS全称“跨域资源共享”Cross-Origin Resource Sharing。CORS是一个HTTP协议层面的机制浏览器在跨域请求时会自动在请求头里带上Origin字段告诉服务器“我是从http://localhost:8080来的”。服务器收到后如果允许这个来源访问就在响应头里返回Access-Control-Allow-Origin: http://localhost:8080。浏览器看到这个响应头才把请求结果交给前端代码。如果服务器没有返回这个头或者返回的值和Origin对不上浏览器就判定为“服务器不认你”直接把响应拦截掉。注意这时候请求可能已经到达后端了接口也可能执行了但浏览器不让你拿到响应。这个特性坑过很多人——有时候你发现后端日志里明明有请求记录但前端就是报CORS错误原因是响应头没配好。CORS还分“简单请求”和“预检请求”两类。简单请求是指GET、POSTContent-Type为application/x-www-form-urlencoded、multipart/form-data或text/plain这类请求浏览器会直接发出但响应时校验CORS头。预检请求是指请求方法比较特殊PUT、DELETE、PATCH或者请求头带了自定义字段、Content-Type不是那三种简单类型此时浏览器会先发一个OPTIONS请求服务器正确响应预检后才会发真正的业务请求。很多Django新手遇到的“POST请求正常PUT/DELETE请求失败”就是预检没处理好。后面我会专门讲配置。2.3 Django为什么默认不处理跨域Django作为Web框架本身只负责处理路由、视图、模板、ORM这些核心逻辑它并不内置CORS响应头的生成。因为CORS本质是“HTTP响应头的附加规则”属于中间件的职责范畴Django默认模板里没有这个需求自然不带上。所以在Django项目里处理跨域标准做法就是安装第三方库django-cors-headers或者手动在中间件里加响应头。两套方案我都试过根据自己的项目规模和需求选择。3. 项目里到底用哪套方案django-cors-headers 和手写中间件对比3.1 老牌方案django-cors-headers 的使用套路这个库是Django生态里最常用的跨域解决方案维护得挺勤快兼容Django 3.x和4.x/5.x都没问题。用法很简单三步走第一步安装pip install django-cors-headers第二步在settings.py的INSTALLED_APPS里注册INSTALLED_APPS [ # ... corsheaders, # ... ]第三步在MIDDLEWARE里添加注意位置很关键文档建议放在CommonMiddleware之前MIDDLEWARE [ # ... corsheaders.middleware.CorsMiddleware, django.middleware.common.CommonMiddleware, # ... ]然后配置允许的来源。最省事但最不安全的方式是CORS_ALLOW_ALL_ORIGINS True这个配置只适合开发阶段测试用一上线必须关掉否则任何网站都能往你的接口发请求等于把后端裸奔在公网上。生产环境推荐显式指定允许的来源CORS_ALLOWED_ORIGINS [ https://admin.example.com, https://www.example.com, ]如果你的前端是本地开发也可以用CORS_ALLOWED_ORIGIN_REGEXES [ r^http://localhost:\d$, ]这个正则表示允许所有本地端口开发期间很好用但生产环境同样要删掉。对于预检请求还需要配置允许的请求方法和请求头。默认情况下django-cors-headers会允许所有Common请求方法和几个默认头但如果你用到了自定义请求头要显式加上CORS_ALLOW_METHODS [ DELETE, GET, OPTIONS, PATCH, POST, PUT, ] CORS_ALLOW_HEADERS [ accept, authorization, content-type, user-agent, x-csrftoken, x-requested-with, ]特别提一下x-csrftoken这是Django的CSRF防护用的自定义头如果前端用了Django的CSRF机制跨域时这个头必须加进允许列表否则预检请求会直接失败。3.2 手写中间件什么时候需要自己轮子有人会觉得“有现成库干嘛不用”但遇到两种情况你得考虑手写中间件。第一种是项目依赖特别老旧或精简不想引入任何第三方包尤其是内网部署环境可能无法访问PyPI。这时候自己写一个中间件只需要十几行代码完全可控。第二种是你需要根据业务逻辑动态决定跨域规则比如根据请求头里的某个参数判断是哪个业务方调用再决定允不允许。django-cors-headers虽然可以通过CORS_ALLOWED_ORIGINS做静态配置但动态判断还得自定义。手写中间件的核心逻辑就是在process_response里给响应对象加上CORS响应头class CorsMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): response self.get_response(request) origin request.META.get(HTTP_ORIGIN, ) # 开发环境允许所有来源生产环境按白名单判断 if origin and (settings.DEBUG or origin in settings.CORS_ORIGIN_WHITELIST): response[Access-Control-Allow-Origin] origin response[Access-Control-Allow-Credentials] true if request.method OPTIONS: response[Access-Control-Allow-Methods] GET, POST, PUT, PATCH, DELETE, OPTIONS response[Access-Control-Allow-Headers] Content-Type, Authorization, X-CSRFToken response[Access-Control-Max-Age] 86400 response.status_code 200 return response注意这里返回的Access-Control-Allow-Origin我写的是请求的origin变量而不是写死具体值因为如果配置了credentials即允许携带Cookie跨域响应头的Access-Control-Allow-Origin不能是通配符*必须回显具体来源才能让浏览器放行。这是CORS规范里的一个细节很多人在这里翻车。3.3 两个方案怎么选我给你的参考建议我的实际经验是能用django-cors-headers就用它的成熟度和社区验证度更高尤其对于常见配置几乎零出错。但前提是你能把它的配置项吃透而不是无脑CORS_ALLOW_ALL_ORIGINSTrue然后发上线。手写中间件适合那些对依赖数量敏感、或者需要动态跨域策略的项目。比如我接手过一个老项目Django版本还停留在2.2但运行在Python 3.6环境下新版本的django-cors-headers对旧版Django支持不太好装老版本又有兼容性隐患这时候手写反而更稳。不管用哪种方案最后都要测三件事GET请求能不能跨域、带自定义头的POST请求能不能过预检、能不能正常携带Cookie如果涉及登录态。这三步测完基本心里有数。4. 完整配置案例从开发环境到生产环境的一次实战配置4.1 开发环境配置怎么配才能既方便又不出错我平时的新项目开发环境习惯这样配。整个settings.py里关于跨域的部分长这样# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, # ... corsheaders, # ... ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.security.SecurityMiddleware, django.contrib.sessions.middleware.SessionMiddleware, django.middleware.common.CommonMiddleware, django.middleware.csrf.CsrfViewMiddleware, # ... ] # 开发环境方便调试先允许所有来源 CORS_ALLOW_ALL_ORIGINS True CORS_ALLOW_CREDENTIALS True # 允许的请求方法 CORS_ALLOW_METHODS [ DELETE, GET, OPTIONS, PATCH, POST, PUT, ] # 允许的自定义请求头前端要带token认证时必配 CORS_ALLOW_HEADERS [ accept, authorization, content-type, user-agent, x-csrftoken, x-requested-with, ]注意这里我把CORS_ALLOW_CREDENTIALS True也打开了因为开发环境前端要测试登录态Cookie和Authorization头都需要跨域携带。但这里有个坑当ALLOW_CREDENTIALSTrue时Access-Control-Allow-Origin不能是*django-cors-headers会自动处理这个问题因为CORS_ALLOW_ALL_ORIGINSTrue时它回显的就是具体Origin这没问题你可以放心用。4.2 生产环境配置只开放给自家前端域名一旦项目要上线必须把开发环境的宽松配置收回来。生产配置我一般这样写# 生产环境禁止全部放开 CORS_ALLOW_ALL_ORIGINS False # 白名单只允许自家前端的域名 CORS_ALLOWED_ORIGINS [ https://admin.mycompany.com, https://www.mycompany.com, ] # 如果前端有多个子域名可以用正则 CORS_ALLOWED_ORIGIN_REGEXES [ r^https://.*\.mycompany\.com$, ] # Cookie跨域必须开 CORS_ALLOW_CREDENTIALS True CORS_ALLOW_METHODS [ DELETE, GET, OPTIONS, PATCH, POST, PUT, ] CORS_ALLOW_HEADERS [ accept, authorization, content-type, user-agent, x-csrftoken, x-requested-with, ]这里有一个特别容易忽略的点如果前端用了http://localhost:8080开发而后端API是https://api.mycompany.com那么生产环境正则不能包含localhost否则等于放开了一个能绕过白名单的入口。虽然这个入口危害不大但从安全角度来看宁缺毋滥。4.3 保存配置后必须跑的自测清单配置完不是就完事了我每次配置跨域都会跑一遍自测保证前端同事不用再半夜找我。第一项测GET打开浏览器开发者工具在Console里执行fetch(https://api.example.com/api/health/)看Network面板里预检请求是否通过响应头里是否有access-control-allow-origin。第二项测带Cookie的POST在Application面板里保证当前页面域名的Cookie能正常种上然后发起POST请求观察是否携带了credentials: include后端能否读到Session。第三项测自定义头前端在fetch里加一个X-Custom-Header: test看浏览器是否先发OPTIONS预检预检响应是否返回了access-control-allow-headers: x-custom-header。这三项全部通过跨域配置基本稳了。如果任何一项失败优先检查请求方法、请求头、来源域名是否在允许列表里。4.4 还有个隐藏配置CSRF怎么办Django默认开启了CSRF中间件所有通过Session认证的POST请求都要求带csrftoken。跨域请求如果涉及登录态前端需要先获取CSRF Cookie然后在请求头里带上X-CSRFToken。如果你后端写的是API接口且用了JWT认证Token放在Authorization头里通常不需要CSRF因为CSRF攻击依赖Cookie自动携带而JWT不依赖Cookie。这种情况下可以局部禁用CSRF比如用csrf_exempt装饰器或者修改中间件配置。但如果你还在用Session登录跨域场景下CSRF配置就会比较麻烦。我的经验是前后端分离项目建议用Token认证而不是Session这样CSRF的复杂度直接消失。如果你实在要用Session确保CORS_ALLOW_HEADERS里加了x-csrftoken并且前端确保在请求头里带上这个值。5. 我在实际项目里遇到过的跨域问题排查和避坑实录5.1 问题一所有请求都正常唯独带Authorization头的请求失败现象很典型前端拿Token登录登录接口POST能用但是每次调用需要认证的接口比如获取用户资料就报CORS错误。打开Network面板看到预检请求OPTIONS返回了403 Forbidden或者直接没返回CORS头。排查过程先看后端日志OPTIONS请求确实到了Django但是响应没有加CORS头。原因是我的Django视图里没有处理OPTIONS请求默认Django会对不认识的OPTIONS请求返回405或者直接被中间件拦截。django-cors-headers的中间件虽然会拦截OPTIONS请求自动返回响应但前提是它得在中间件链里足够靠前并且在请求处理流程里能正确识别。后来确认是我的自定义中间件排在CorsMiddleware前面把OPTIONS请求先返回了。解决办法是把CorsMiddleware放在最前面至少在CsrfViewMiddleware之前因为Django的CSRF中间件也会校验POST请求导致的405或403会跳过CORS头处理。这个坑提醒我中间件的顺序对CORS的影响很大。django-cors-headers官方文档其实有说明建议放在CommonMiddleware之前但很多项目在Django 3.x里还会默认存在CsrfViewMiddleware一旦顺序不对OPTIONS预检就被CSRF卡死了。5.2 问题二开发环境跨域没问题生产环境就失败这个我一开始没想明白后来才发现是Nginx的锅。前端项目通过Nginx反向代理到后端Django但生产环境的Nginx配置只对/api/路径做了反代前端页面跑在/路径下结果前端向/api/发请求时请求头的Origin是https://www.example.com后端看到的请求路径是http://127.0.0.1:8000/api/...响应头里Django加的Access-Control-Allow-Origin是https://www.example.com理论上没问题。但实际浏览器报错原因在于Nginx把响应头里的Access-Control-Allow-Origin给覆盖了或者去掉了。有的Nginx配置会在location块里添加自定义响应头比如add_header X-Content-Type-Options nosniff;这个add_header一旦出现Nginx除默认响应头外不会保留上游Django传过来的其他自定义头除非你显式加上一行add_header Access-Control-Allow-Origin $http_origin;这才是根源。所以如果你在生产环境用了Nginx排查跨域问题时一定要看Nginx的响应头配置而不仅仅是Django里的配置。5.3 问题三Cookie跨域设置Secure和SameSite属性还有个更隐蔽的坑前端登录成功后端在Response里set_cookie浏览器也收到了Cookie但是后续请求就是不带。这个问题不仅涉及CORS还涉及Cookie的SameSite属性。Chrome浏览器从80版本开始默认把Cookie的SameSite设为Lax这意味着跨站请求包括跨域XHR默认不携带Cookie。如果你的后端响应Cookie时没有设置SameSiteNone和Secure浏览器就不会在跨域请求中带上它。Django里设置Cookie时可以这样response.set_cookie( sessionid, valuesession_key, max_age86400, httponlyTrue, samesiteNone, secureTrue, )注意SameSiteNone要求SecureTrue也就是说Cookie必须通过HTTPS传输。如果你本地开发是HTTP那就只能用SameSiteLax但Lax跨站就不带这就陷入死锁。解决思路是开发环境也用HTTPS访问前端比如用localhost的证书或者开发时暂时不用跨域Cookie改用JWT放Authorization头。我后来建议团队全面切换到了JWT认证Cookie跨域这摊子事就不用再头疼了。如果你还在用Session跨域Cookie方案这条坑一定绕不过去。5.4 问题四预检请求响应200了但浏览器还说失败有一种情况很诡异Network面板里能看到OPTIONS请求返回200响应头也有Access-Control-Allow-Origin: *但console还是报错。仔细看发现预检请求返回的Access-Control-Allow-Headers里没有前端实际使用的Authorization头。浏览器校验预检响应时会检查Access-Control-Allow-Headers是否包含了前端请求头里的所有自定义头。如果前端带了Authorization但预检响应只写了Content-Type, X-CSRFToken浏览器同样会拦截后续真实请求。所以配置CORS_ALLOW_HEADERS时我习惯把所有可能用到的头都加进去宁可多配不可少配。django-cors-headers默认的列表也包含了常见的头但你自定义的X-Tenant-ID、X-Request-ID之类的要手动加。5.5 附一个快速排查清单如果你现在遇到跨域问题按这个清单排查大概率能定位问题排查项检查内容请求是否真正跨域协议、域名、端口是否全部一致请求方法CORS预检OPTIONS请求是否返回了Access-Control-Allow-Origin、Allow-Methods、Allow-Headers中间件顺序CorsMiddleware是否在CommonMiddleware/CsrfViewMiddleware之前请求来源白名单前端页面的Origin是否在CORS_ALLOWED_ORIGINS中是否被正则匹配自定义请求头所有自定义头是否都加入了CORS_ALLOW_HEADERSCookie跨域属性是否设置了SameSiteNone和SecureTrueHTTPS场景反向代理拦截Nginx或其他代理是否覆写了响应头或过滤掉了Access-Control-*HTTP vs HTTPS浏览器不允许HTTPS页面访问HTTP接口反之http页面https接口一般也有限制每一条都在线下真实环境里踩过记录下来纯粹是想让大家少走弯路。6. 一个容易被忽略的隐藏问题Django DEBUG状态和跨域配置的联动很多人在开发环境顺手写了CORS_ALLOW_ALL_ORIGINS True上线前忘了改结果接口就变成了“全网可调用”。这个说起来很基础但我真的见过不止一次。还有一个更隐蔽的联动如果你的settings.py里基于DEBUG去切换跨域配置要格外小心。我习惯这样写if DEBUG: CORS_ALLOW_ALL_ORIGINS True else: CORS_ALLOW_ALL_ORIGINS False CORS_ALLOWED_ORIGINS [...]但Django的DEBUG在部署环境里如果被设为False这个分支就是安全的。怕就怕某些人直接把生产环境的DEBUG开着跑性能和安全同时崩跨域配置也跟着全放开了。所以上线前一定要强制检查DEBUGFalse且CORS_ALLOW_ALL_ORIGINSFalse。另外CORS_ALLOW_CREDENTIALSTrue配合CORS_ALLOW_ALL_ORIGINSFalse时django-cors-headers会自动匹配CORS_ALLOWED_ORIGINS里的具体来源并回显你不需要自己处理*变回显的问题这可以说是用第三方库相比手写中间件的一个省心点。7. 从“能用”到“安全”跨域配置的安全边界思维跨域问题的本质是“信任边界”问题。浏览器默认不信任跨站请求那么后端要回答“我信任谁的来源”。很多教程只教你怎么把*开起来却不说清楚什么时候该关掉。我的建议是除非你的API是中立的公共API比如天气数据谁都能调否则千万不要生产环境全开。一旦全开攻击者可以构造一个恶意页面在用户浏览器里向你的API发请求。如果用户恰好登录了你的网站并且带着有效的Cookie攻击者就能利用用户的身份执行操作前提是你的API没做CSRF防护且用了Cookie认证。即使你用JWT攻击者没法拿到用户存在内存或localStorage里的Token但如果前端有其他漏洞导致Token泄露整个防线也会崩。所以说跨域白名单虽然不解决所有安全议题但它是一道不应该随意打破的防线。我给出的安全配置范式是只用CORS_ALLOW_ALL_ORIGINS True于本地开发生产环境列出所有前端域名尽量不用正则如果实在要用只匹配自己公司的域名后缀尽量不用Cookie做认证改用Authorization头传Token如果用了Cookie必须SameSiteNoneSecure 单独的认证Cookie而不是直接复用Session定期检查响应头里是否意外出现了Access-Control-Allow-Origin: *。把这些记住了跨域就不会在你睡觉的时候坑你。8. 最后一个实战建议跨域测试用 curl 还是浏览器很多技术人员排查跨域只盯着浏览器但我发现用curl反而能帮你分清“后端问题”和“浏览器问题”。当你用curl -H Origin: http://localhost:8080 -i http://127.0.0.1:8000/api/login/时curl不会像浏览器那样强制CORS限制它只会原样返回响应头所以你能看到Django到底有没有生成Access-Control-Allow-Origin。如果curl能看到这个响应头但浏览器报错那问题多半出在浏览器和前端代码之间比如请求头带的东西不对、预检没过。如果curl也看不到响应头说明后端配置就没生效先回去检查中间件。这是我在排查过程中最常用的“一分为二”技巧能省掉大量猜谜时间。最后再提一个小技巧前端开发时如果你实在没法等后端配置可以在本地用代理插件把请求代理到后端路径但这只是权宜之计生产环境该配的跨域一个都省不掉。真正解决一条龙问题还是得把后端CORS中间件的响应头配明白跑通预检再考虑Cookie或Token的细节。等你在浏览器控制台看到熟悉的Access-Control-Allow-Origin和一条绿色的200你就知道这回是真的稳了。