免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

ASP.NET WebForm一般处理程序跨域解决方案详解

ASP.NET WebForm一般处理程序跨域解决方案详解 最近接了个外包维护的老项目ASP.NET WebForm框架客户要在现有系统里嵌入一个第三方平台的查询页。后端又不想专门搭一套新服务我就顺手用一般处理程序.ashx写了两个接口原本心想几分钟就能搞定的事结果前端一联调控制台直接给我甩了个大红叉Access to XMLHttpRequest ... has been blocked by CORS policy。我第一反应以为是后端没响应用F12打开Network一看请求明明到了服务器响应也正常回来了就是浏览器因为跨域把数据拦在了门外。后来查了一圈发现这问题在老项目里实在太常见了几乎每个从WebForm时代走过来的开发都碰过。今天就把解决过程和思路完整写出来给正被“.NET WebForm 一般处理程序跨域”困扰的朋友做一个参考。1. 先弄清楚一般处理程序为什么会跨域1.1 同源策略到底拦的是什么很多新手容易把跨域误认为服务器拒绝请求其实恰恰相反请求已经发出去了响应也回到浏览器了但浏览器基于同源策略不让页面里的JavaScript读到这个响应。所谓“同源”指协议、域名、端口三者完全一致。只要有一个不同就是跨域。比如页面在https://a.example.com接口在http://localhost:8081端口和协议都不一致那这就算跨域。浏览器之所以这么做是为了防止恶意网站通过脚本偷走其他站点上的数据这是Web安全模型的根基。你可以把它理解成一道门禁门禁是站在浏览器这一侧的服务器是否欢迎你那是另一回事。如果服务器确实想允许某个域名的页面来读取数据就必须明确告诉浏览器这个来源可以访问。而这个“明确告诉”的方式就是通过HTTP响应头里的Access-Control-Allow-Origin字段。所以解决跨域问题的唯一核心思路是让服务端的响应里带上正确的CORS头部信息把浏览器的门禁“解锁”。这不是前端改一下URL就能绕过的服务端必须参与。1.2 一般处理程序在WebForm中的定位ASP.NET WebForm时代一般处理程序.ashx是很多人眼里的“轻量级接口入口”。它不像ASPX页面那样有一堆控件生命周期和ViewState也不像WebAPI那样需要额外配置路由它只是很单纯地接收HTTP请求处理完后输出一段内容。对老项目做小功能改造、输出JSON、生成验证码文件、处理文件上传非常顺手。但也正因为“单纯”一般处理程序默认不携带任何CORS相关响应头。页面本身的ASPX如果是同源部署浏览器自然不拦。可一旦你打算把这个.ashx当作一个独立接口给另一个域名下的页面、小程序端、第三方系统调用跨域问题就会立刻爆发。我在实际项目里最常碰到的场景是这样的老系统里已经有一套业务逻辑写在某个.ashx里现在新前端要跨域调用不能在ASPX页面里直接写Ajax。这时候大家的第一反应是“我加个WebMethod或写个API吧”但问题是老项目不敢大动尽量只改一个文件。于是.ashx就成了最合适的选择在原有文件里加几行响应头代码问题就能解决。1.3 简单请求和预检请求别搞混CORS里有一条挺容易踩坑的分界线简单请求和预检请求。简单请求指的是使用了GET、POST注意Content-Type不能是application/json且没有自定义请求头。简单请求浏览器会直接发出然后在响应里看有没有Access-Control-Allow-Origin有就放行没有就拦截。预检请求则是在正式请求之前浏览器先发一个OPTIONS请求去问服务器“我接下来要POST一个JSON、带上自定义的Token头你允不允许”服务器需要回复允许的方法和请求头浏览器确认没问题以后才会继续发真正的请求。如果服务器没有对OPTIONS请求做处理或者返回的状态码不是2xx正式请求一样会被拦截。一般处理程序里最容易漏掉的就是预检请求处理。很多人手动加了响应头结果发现Ajax还是报错打开Network一看多了一个OPTIONS请求状态码可能是404或者500。原因很简单.ashx默认没有处理OPTIONS的代码IIS有时候还会直接拒掉这个动词。所以我每次给别人讲跨域都会强调一个点CORS不仅是“加一个响应头”而是要把预检请求的处理和响应头的完整组合都做对。2. 方案选型CORS为主JSONP为辅2.1 CORS的核心机制CORS全称是Cross-Origin Resource Sharing跨域资源共享。它不像JSONP那样靠动态插入script标签“钻空子”而是浏览器和服务器通过HTTP头进行协商。服务器在响应里返回一个策略声明浏览器根据这个声明决定是否允许当前页面读取数据。最关键的几个响应头包括响应头作用Access-Control-Allow-Origin允许哪个源访问可以是具体域名也可以是*Access-Control-Allow-Methods允许哪些HTTP方法如GET、POST、PUT、DELETE、OPTIONSAccess-Control-Allow-Headers允许哪些自定义请求头如Content-Type、AuthorizationAccess-Control-Allow-Credentials是否允许携带Cookie值为true时Allow-Origin不能是*Access-Control-Max-Age预检请求结果可以缓存多少秒减少重复OPTIONS请求理解CORS后方案其实就清楚了让服务端正确返回这些头。具体怎么做有几种路径我下面按推荐程度从高到低说。2.2 方案一在.ashx代码中直接加响应头最直接的方式就是在一般处理程序的ProcessRequest方法里手动追加响应头。代码很简单public void ProcessRequest(HttpContext context) { context.Response.AddHeader(Access-Control-Allow-Origin, *); context.Response.AddHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); context.Response.AddHeader(Access-Control-Allow-Headers, Content-Type, X-Requested-With); // 你的业务逻辑 context.Response.ContentType application/json; charsetutf-8; context.Response.Write({\code\:0,\msg\:\ok\}); }这种方式的好处是只影响当前这个处理程序不会给整个站点“开门”。坏处是如果项目里有几十个.ashx每一个都要复制粘贴后期维护容易漏。如果你只有两三个接口我建议直接这么干简单粗暴有效。2.3 方案二Web.config全局加响应头如果站点里有大量接口都要跨域那就可以在Web.config的system.webServer节点里统一加自定义响应头system.webServer httpProtocol customHeaders add nameAccess-Control-Allow-Origin value* / add nameAccess-Control-Allow-Methods valueGET,POST,PUT,DELETE,OPTIONS / add nameAccess-Control-Allow-Headers valueContent-Type, X-Requested-With / /customHeaders /httpProtocol /system.webServer这种做法的优势是全局生效不用改C#代码发布时只需替换配置文件。但要注意一个问题它只负责“加响应头”不负责“处理OPTIONS预检请求”。预检请求到达IIS时如果没有任何处理程序响应它还是会返回405或404。所以Web.config方案通常还需要和Global.asax或某个专门处理OPTIONS的模块配合。另一个坑是Web.config自定义头是静态的写死了*如果以后想针对不同域名返回不同的Allow-Origin这就做不到。对于内部管理系统我认为还是动态判断更安全。2.4 方案三在Global.asax里统一处理如果项目本身有Global.asax那么利用Application_BeginRequest事件可以做到全局统一处理而且能拦截OPTIONS请求代码示例protected void Application_BeginRequest(object sender, EventArgs e) { HttpContext context HttpContext.Current; context.Response.AddHeader(Access-Control-Allow-Origin, *); context.Response.AddHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); context.Response.AddHeader(Access-Control-Allow-Headers, Content-Type, X-Requested-With); if (context.Request.HttpMethod OPTIONS) { context.Response.StatusCode 204; context.ApplicationInstance.CompleteRequest(); } }注意在Global.asax里尽量不要用context.Response.End()因为会抛ThreadAbortException虽然能生效但会产生一个不友好的异常记录。用CompleteRequest()来结束请求更干净。这个方案对于整个站点可能有多个.ashx、ASPX都需要跨域时最省事一个地方统一管。但如果站点里有些页面不应该对外开放跨域全局处理会导致所有页面都被放开这时候我建议在方案一和方案二之间取舍。2.5 为什么不推荐JSONP作为首选JSONP也是一种经典方案它利用script标签不受同源策略限制的特点让服务器返回一段JS代码来执行回调。在.ashx里可以这样实现public void ProcessRequest(HttpContext context) { string callback context.Request[callback]; string data {\code\:0,\msg\:\ok\}; if (string.IsNullOrEmpty(callback)) { context.Response.ContentType application/json; charsetutf-8; context.Response.Write(data); } else { context.Response.ContentType application/javascript; charsetutf-8; context.Response.Write(string.Format({0}({1});, callback, data)); } }前端用$.ajax({ dataType: jsonp })就能调用。但JSONP有两个先天性限制只能发GET请求没法做POST而且因为它是动态执行JS的能力安全性要比CORS低一些。如果回调参数没做校验攻击者可能构造一个特殊的callback名造成XSS风险。现在的主流项目中我只有在服务器不能改响应头、或者被迫兼容IE旧版本时才考虑用JSONP。一般处理程序本身就在你手上能用CORS就尽量用CORS。3. 实操过程写出一个可跨域的一般处理程序3.1 准备工作先说下环境。我这里用的是Visual Studio 2019目标框架是.NET Framework 4.6.1部署环境是Windows Server 2016上的IIS 10。不管是.NET Framework 3.5还是4.8思路是一样的只是项目文件结构略有差异。新建一个ASP.NET Web Forms项目后右键“添加”-“新建项”选择“一般处理程序”命名为UserHandler.ashx。文件创建出来后系统会自动生成两个文件.ashx本身和.ashx.cs代码文件。我们主要在.cs文件里编码。在动手之前建议先确认几个事情接口是给哪个域名下的页面调用如果是多个域名后面需要考虑动态白名单。请求方式是什么是纯GET还是带着JSON的POST请求头里有没有自定义的Authorization、X-Token这类字段这些信息决定了你后续要设置哪些响应头。如果只是GET请求Allow-Headers可以少配如果前端的Content-Type是application/json那就必须处理OPTIONS预检并且允许Content-Type请求头。3.2 改造.ashx的核心代码下面给出一份我在生产环境里常用的写法它添加了CORS头也处理了预检请求public class UserHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpRequest request context.Request; HttpResponse response context.Response; response.AddHeader(Access-Control-Allow-Origin, *); response.AddHeader(Access-Control-Allow-Methods, GET, POST, OPTIONS); response.AddHeader(Access-Control-Allow-Headers, Content-Type, X-Requested-With); response.AddHeader(Access-Control-Max-Age, 1728000); if (request.HttpMethod OPTIONS) { response.StatusCode 204; response.End(); return; } // 业务逻辑入口 if (request.HttpMethod GET) { response.ContentType application/json; charsetutf-8; response.Write({\code\:0,\msg\:\get success\}); } else if (request.HttpMethod POST) { // 读取请求体 string input; using (var reader new System.IO.StreamReader(request.InputStream)) { input reader.ReadToEnd(); } response.ContentType application/json; charsetutf-8; response.Write({\code\:0,\msg\:\post success\,\received\:\ input \}); } } public bool IsReusable false; }我在代码里加了Access-Control-Max-Age让浏览器缓存预检结果20天。这样做的好处是预检请求只会在第一次调用时发出减少一次OPTIONS往返前端响应速度会快一些。实际项目里如果调用很频繁这个头值得加。有一个细节我要提醒一下很多人习惯用Response.AddHeader追加如果同一个响应头被加了两次比如Web.config里已经配置了Access-Control-Allow-Origin代码里又Add了一次浏览器收到的响应头里就会有两个Access-Control-Allow-Origin这个会被判定为非法值照样报CORS错误。所以要么统一在配置文件里配要么统一写在代码里别两边都来。3.3 预检请求的完整处理上面代码里我判断了request.HttpMethod OPTIONS然后返回204。为什么状态码用204而不是200因为预检请求不需要返回任何业务内容只需要告诉浏览器“服务器允许这次请求”响应体为空使用204 No Content最合适。在实际调试时我发现有些开发会忘了在OPTIONS分支里返回结果预检请求直接穿过代码走到了下面的业务逻辑把业务代码执行了一遍然后返回一个正常的200 JSON。浏览器拿到这个200 JSON后会先判断它有没有CORS头有的话其实也行。但是这么做有一个隐患业务逻辑被无端的OPTIONS请求给触发了如果那段业务里有插入数据库或者发送邮件之类的操作就会造成重复执行。所以预检请求一定要在入口处直接短路掉不碰业务。还有一种情况是IIS对OPTIONS请求做了严格筛选会直接返回405。处理办法有两种打开IIS管理器选中站点找到“请求筛选”在“HTTP谓词”里添加一个OPTIONS设为允许。或者在Web.config的system.webServer/security/requestFiltering节点下允许OPTIONSsystem.webServer security requestFiltering verbs add verbOPTIONS allowedtrue / /verbs /requestFiltering /security /system.webServer排障的时候建议先用Fiddler或Postman手动发一个OPTIONS请求看看服务端到底返回什么。如果手动请求都返回405那就不是CORS配置的问题而是IIS层面把OPTIONS拦了。3.4 前端Ajax联调示例服务端配置好后前端那边也要稍微注意一下。如果只是最简单GET请求jQuery里这样写就行$.ajax({ url: http://api.sample.com/UserHandler.ashx, type: GET, dataType: json, success: function (res) { console.log(res); }, error: function (xhr, status, error) { console.log(status, error); } });dataType不要设置成jsonp否则jQuery会强行用JSONP模式去请求。如果是POST JSON而且想携带Cookie或登录态那就需要多做两步$.ajax({ url: http://api.sample.com/UserHandler.ashx, type: POST, contentType: application/json, data: JSON.stringify({ name: 张三, age: 30 }), xhrFields: { withCredentials: true }, success: function (res) { console.log(res); } });这里withCredentials: true表示跨域请求要带上Cookie。此时服务端的Access-Control-Allow-Origin不能是*必须明确返回当前请求的Origin并且要加Access-Control-Allow-Credentials: true。这一点我在第四节还会展开讲。3.5 验证响应头是否生效配置完成后我习惯用Chrome的开发者工具验证。先按F12打开DevTools切换到Network面板刷新页面后找到对应的请求点击它在Headers里看Response Headers区域。正常情况下能看到类似这样的头Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Requested-With如果能看到这些说明服务端已经允许跨域。如果看不到说明代码没生效或者请求被IIS拦截了又或者是服务器上有反向代理把响应头吃掉了。还有一种情况浏览器有缓存旧响应头被缓存住了按CtrlF5强制刷新再验证。如果用的是Firefox可以在存储里看但Chrome的Network面板已经够用了。4. 常见跨域问题与排查技巧实录4.1 加了响应头仍被CORS拦截这是出现频率最高的问题明明在代码里写了AddHeader(Access-Control-Allow-Origin, *)结果浏览器还是拦截Network里的响应头也看不到这个字段。我排查过几次原因通常是以下三个中的一个。第一代码里用了Response.Write之后才加Header或者Header加错了地方。ASP.NET里一旦开始向客户端输出内容再往Response里追加Header就会抛出“此时无法添加响应头”的异常或者虽然没抛异常但头部因时序问题没送到客户端。正确的顺序是先设置Header再Write内容。第二Web.config里httpProtocol/customHeaders也加了一个Access-Control-Allow-Origin代码里又加了一个导致响应头出现两个同名值。Chrome会把它识别为无效直接拦截。排查方法是在Network面板里看响应头里有没有重复字段有的话去掉其中一个。第三请求经过了一个反向代理比如Nginx、IIS ARR代理只转发了请求没有把后端的响应头完整地透传回来或者代理本身添加了一个覆盖默认值的头。这种情况需要检查代理层的配置比如Nginx里要注意proxy_hide_header和add_header。4.2 OPTIONS返回405或404前面提过405通常是IIS请求筛选不允许OPTIONS动词导致的。404则可能是路由配置把OPTIONS请求转发到了一个不存在的处理器。一般处理程序的IsReusable属性不会影响这个问题大概率出在IIS的Handler Mapping上。排查建议用Postman直接发OPTIONS请求看返回状态码。如果返回405到IIS的“请求筛选”里加OPTIONS谓词。如果返回404检查站点是否有默认文档或URL重写规则把请求转发到了别的路径。老项目里很可能装过URL Rewrite模块规则会拦截OPTIONS。还有一种情况IIS 7之前的老版本对OPTIONS处理不太友好如果你还在用IIS 6建议考虑在Web.config里用httpHandlers给OPTIONS单独配置一个Handler或者升级部署环境。4.3 跨域带Cookie的认证问题跨域不单是响应头的事一旦牵扯到Cookie认证问题会复杂得多。举个例子WebForm系统用FormsAuthentication写了一个登录态Cookie前端页面在b.example.com要跨域调用a.example.com上的.ashx并且希望.ashx里还能拿到登录用户的身份。这需要同时满足三个条件服务端返回Access-Control-Allow-Origin: http://b.example.com不能是*服务端返回Access-Control-Allow-Credentials: true前端请求设置xhrFields: { withCredentials: true }如果这三者任何一个不满足浏览器都会在Network里报CORS错误。很多时候前端以为“我设置了withCredentials就够了”其实服务器那边漏掉了Allow-Credentials照样不行。另外这里还要考虑Cookie的SameSite属性。如果.ashx所在的站点设置Cookie时写了SameSiteLax或SameSiteStrict那么跨站请求可能不会携带这个Cookie导致即使CORS解决了后端依然认为是未登录状态。这个问题在.NET WebForm里不太起眼但如果你的站已经开始混用HTTPS和多域名就必须认真查一下Cookie的SameSite设置。4.4 JSONP被“坑”的地方有些老系统确实还在用JSONP所以踩坑记录也不得不提。最常见的问题有两个。第一前端$.ajax的dataType设置成jsonp后如果服务端没有正确返回回调包裹格式前端会报“Invalid label”之类的错误。尤其是服务端代码忘了读取callback参数直接返回了一个普通JSON对象浏览器把它当成JS执行时就会解析失败。第二JSONP只支持GET这意味着你没法用它传输很大的数据也没法使用DELETE、PUT这些方法。如果业务本身需要POSTJSONP这条路就断了必须回到CORS。还有一个安全细节类型检查。服务端在拼接回调用字符串时一定要校验callback参数是否只包含英文字母、数字、下划线和点号否则攻击者传入callbackalert(1)之类的参数等于给了别人一个XSS执行窗口。我的建议是能不用JSONP就不用JSONP已经用的至少把请求的Referer校验和callback白名单校验加上。4.5 调试阶段的小工具和思路排查跨域问题我通常会先开两个工具一个是Postman一个是Chrome的Network面板。Postman不受浏览器同源策略限制它能直接看到服务端到底返回了什么响应头。如果在Postman里能看到CORS头而浏览器里看不到基本都是浏览器缓存或代理问题如果在Postman里也看不到那问题就在服务端代码和配置层。还有一个思路用curl命令手动测试特别是需要精确指定Origin头时curl比Postman更方便。curl -i -H Origin: http://b.example.com http://api.sample.com/UserHandler.ashx这个命令会把响应头完整打出来一眼就能看出有没有Access-Control-Allow-Origin。我在Windows上装了curl排错效率提升不少。5. 生产环境的安全思考与扩展建议5.1 不要对所有人开放“*”开发环境里为了省事Access-Control-Allow-Origin: *是没问题。但生产环境这么写等于允许互联网上任何一个网站都能调用你的一般处理程序接口这可能会被第三方网站恶意利用。比如你的.ashx返回用户信息某个恶意页面在自己的站点里发Ajax请求浏览器因为CORS允许就会把响应数据读出再传回攻击者的服务器。如果接口依赖Cookie认证攻击者无法带上受害者的Cookie所以风险还相对可控但如果接口本身不需要登录那就等于裸奔。我在生产环境的建议是把*改成固定来源。只允许自己的前端域名跨域调用其他来源一律不返回CORS头或者直接返回403。5.2 动态白名单与Vary头当你面对多个合法来源域名时可以做成动态白名单。比如从数据库或配置文件中读允许列表然后判断当前请求的Origin是否在列表里存在才返回对应的Allow-Originstring[] allowedOrigins new string[] { http://a.example.com, https://b.example.com }; string origin context.Request.Headers[Origin]; if (!string.IsNullOrEmpty(origin) allowedOrigins.Contains(origin)) { context.Response.AppendHeader(Access-Control-Allow-Origin, origin); context.Response.AppendHeader(Vary, Origin); context.Response.AppendHeader(Access-Control-Allow-Credentials, true); } else if (string.IsNullOrEmpty(origin)) { // 同源或非跨域请求不做特殊限制 } else { context.Response.StatusCode 403; context.Response.End(); }这段代码里加了一个Vary: Origin它是给代理和浏览器缓存看的表示“当前响应的结果会根据Origin头动态变化”。如果不加这个头缓存服务器可能把一个老来源的响应缓存之后给另一个新来源复用造成诡异的问题。这个细节很多人会忽略但在生产环境一旦遇到缓存错乱排查起来会特别痛苦。另外动态白名单的好处是允许了跨域携带Cookie。因为这时Allow-Origin返回的是具体来源而不是*加上Allow-Credentials: true后登录态的跨域请求才能正常工作。Web.config静态配置很难做到这一点所以涉及到多个前端来源时我更推荐代码动态处理。5.3 是否有必要升级到WebAPI有些读者可能会问既然WebForm项目做接口这么麻烦为什么不直接升级到ASP.NET WebAPI或者现代的最小API这个问题我不能一概而论。如果整个项目只是零星几个接口要给第三方调用改造WebAPI的成本反而高你需要配置路由、处理异常过滤器、写新的控制器还得考虑和老项目Session、认证机制的兼容。这时候一般处理程序加CORS就是性价比最高的方案。但如果你的接口数量已经超过十个而且还在快速增加那确实应该考虑把接口层独立出去用WebAPI或者.ashx自定义框架都行重点是把跨域、认证、日志这些横切面统一管理起来。我见过不少项目最初靠.ashx简单加CORS头撑着后来接口越来越多每个文件里都塞满了重复的CORS代码到最后改起来四处漏水。所以这个选择其实不是技术问题而是维护压力的权衡。我个人在实际操作中的体会是老项目改造一定要“小步快跑”能在一个文件里解决的就不要为了架构优雅而去重构到伤筋动骨。跨域问题看起来只是几个响应头的事但只要把CORS的原理吃透——同源策略怎么拦、预检请求什么时候触发、响应头怎么配合Cookie——无论以后是WebForm还是WebAPI都能快速落地。最后一次踩坑是在一个部署了五年没动过的服务器上IIS默认的OPTIONS拦截让前端报了一下午错最后发现Web.config里加一行允许OPTIONS的配置就解决了。这些细节文档里很少会主动提醒你但实际生产中真的能救命。
返回列表