上一篇写到利用Cross进行AJAX跨域,简单的略过了,因为网上有太多的文章说的比我好很多,本人不是很擅长写博客,比较懒。这篇开始说说重点问题。
问题一、跨域ajax提交时,当携带headers头时,请求将报错。无法执行!上代码
$("#btnPost").click(function () { $.ajax({ type: "post",url: "http://localhost:8021/Property.Api/Product/FullProducts",beforeSend: function (request) { request.setRequestHeader("Authorization","Basic " + localStorage["Token"]); },data:{Name:"张三"},success: function (response) { if (response.Code != undefined) { document.getElementById("result").innerHTML += "<div>消息为:" + response.Message + "</div>"; return; } var tb = $("<table border=1><td>名称</td><td>价格</td><td>排名</td></table>"); for (var product in response) { tb.append("<tr><td>" + response[product].Name + "</td><td>" + response[product].Price + "</td><td>" + response[product].Code + "</td></tr>"); } $("#list").append(tb); },error: function () { document.getElementById("result").innerHTML += "<div>post err</div>"; } }); });
重点在于request.setRequestHeader("Authorization","Basic " + localStorage["Token"]); 。
应用场景,当做一个web app,使用web api作为服务端的时候,相信和我一样的小白,总会想到如何做身份验证。(最近在研究phonegap)
网上的做法:
1、输入用户名、密码
2、登录时 跨域 请求 服务器(web api端),服务器验证帐号密码之后发送一个令牌,返回给客户端
3、客户端登录之后每次请求数据时,携带令牌给服务器去请求数据,服务器验证令牌是否合法,合法返回请求,不合法返回未授权。常规的说法是header上加入令牌信息,正如上面的代码一样。
那么、我上面的代码在正常的情况下是否能请求成功呢???答案肯定是,不能!否则我也不会在此废话了。上代码
public static class tt { public static int i = 0; } public class ProductController : ApiController { public List<Product> Products = new List<Product>() { new Product(){Name="金丝大环刀",Code="No.1",Price=8888,Created=DateTime.Now},new Product(){Name="天地阴阳招",Code="No.2",Price=5555,new Product(){Name="乌木剑",Code="No.3",Price=100,Created=DateTime.Now} }; [AllowAnonymous] public string GetToken() { return "123"; } public List<Product> FullProducts() { tt.i++;//检测是否重复提交 Products[0].Price = tt.i; return Products; } }
其中tt是我用于测试的,待会下一个问题会用到,直接看下面的productcontroller即可,很简单的2个方法,现在当我们请求时看看会发生什么事。
报错了,没错。你看到的是报错了。那么,为什么会报错呢?我们明明已经允许了跨域了的,为什么还会报错????
我们看到上面有个Options的请求,这个具体是干什么呢?
原来在跨域请求时 ( 具体经过测试应该是跨域请求post携带header头时,因为我第一个按钮是get请求不会执行这个options请求 )
浏览器会与服务器做一个自检的请求,具体应该是请求服务器的返回头,看是否允许跨域等(详细的请百度)。
那么,问题产生了,我们应该怎么解决?毕竟我们做验证时放在header头是比较优雅的做法,而如果直接把令牌放在每次请求的参数里的话,,客户端调用时就有点工作量了,虽然可以封装ajax请求,达到自动每次携带的目的,但是服务器端去解析和正常参数混在一起的token,感觉始终那么的混乱。。。。所以,我们还是想把令牌放在header里,非常想,优雅的想。。。。。。
解决方案1:客户端关闭options请求,具体的代码为request.setRequestHeader("X-Requested-With",null]);、此方法为在谷歌上搜索到的方案,本人没有具体测试。
缺点很明显:你应该尽量少去要求客户(调用者)做更多的事。调用者每次都去加这么一个东西,想想就麻烦,别人干脆不用了。
解决方案2:从web api自身解决。那么应该怎么解决呢?直接贴代码了。
[HttpOptions] [HttpPost] public List<Product> FullProducts() { tt.i++;//检测是否重复提交 Products[0].Price = tt.i; return Products; }
也就是在我们具体的action上打上 HttpOptions标签,并且如果是Post或get请求,还必须打上Httppost或httpGet标签
注意:这种情况是Web api 在默认的路由情况下,也就是如下:
那么,会发生什么情况呢???????????
看效果图。
你会发现,调用时设置headers成功了。。但是出现了一个严重的问题,就是调用的action实际上被执行了2次,这也就是图1时我写tt一个计数器的原因。
这种错误是无法忍受的,那么如果设置成默认的路由方式,这个问题我我们无法忍受的。。。
那么如何解决这个问题呢??????????难道在每个action里去写这样的代码。
if( request.Method.ToString().ToLower() == "options")
{
return ......
}
else
业务代码...............
现在有2个头疼的问题就产生了。
问题1:每个action上必须打上 [HttpOptions] =》好操蛋
问题2:每个action要去做请求类型判断 。=》稍微有点经验的人,会发现其实这个问题在N多地方可以解决。
现在我们先不解决这个问题,看下如果改变默认的路由,改成mvc的默认路由方式,映射到action之后会出现些什么问题。
出现的问题,和默认路由如出一辙、action方法依然被执行了2次。
这里声明下:采用默认路由方式的时候其实还有另外一种解决方案是在每个action 加一个 puclic string Options的方法,返回null即可。因为默认路由是根据请求的动作去匹配具体方法的。所以上面说的默认路由的问题,其实是存在于自定义成MVC方式路由产生的。而默认以请求动作的路由时也同样会产生相同问题。
问题总结:
一、采用默认的路由{controller}/{id}时,web api是根据请求动作去匹配action的。
所产生的问题是需要每个控制器都加上一个puclic string Options的方法,
二、采用MVC方式的默认路由{controller}/{action}/{id}时
所产生的问题是需要在每个具体的action上加上 [HttpOptions]的标签,并且需要做请求判断,否则action里的代码会执行二次。
解决办法:
如果采用默认的路由{controller}/{id}时,可以自定义一个ApiControllerBase,里面写一个Action 。
public virtual string Options()
{
return "自检";
}
如果采用MVC路由方式{controller}/{action}/{id}时,则需要在每个action打上【httpOptions】标签,并做逻辑判断,否则action会被执行2次。
万金油办法:
思路:如果路由可以这样干,那么就完美了、当请求为Options请求时,固定路由到某个Action,此Action返回空方法。
从路由模版去定义貌似实现不了这样一个需求,经过多方考察,终于让我找到了如意的解决方案。
/// <summary> /// 防止跨域时Options自检引起的重复提交重复提交 /// </summary> public class OptionsConstraint : IHttpRouteConstraint { public bool Match(System.Net.Http.HttpRequestMessage request,IHttpRoute route,string parameterName,IDictionary<string,object> values,HttpRouteDirection routeDirection) { if (request.Method.ToString().ToLower() == "options") { if (parameterName != null) { values[parameterName] = "Options"; } } return true; } }
在路由的第四个参数加上
//默认路由
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { action = new OptionsConstraint() }
);
最后写一个控制器基类。
public abstract class ApiControllerBase : ApiController
{
[HttpOptions]
public virtual string Options()
{
return "自检";
}
}
让自己的控制器,继承自ApiControllerBase
此方案适合各种路由配置,不管是以api默认路由方式,还是mvc路由到action的方式。