WebAPi Restful 的实现和跨域

现在实际开发中用webapi来实现Restful接口开发很多,我们项目组前一段时间也在用这东西,发现大家用的还是不那么顺畅,所以这里写一个Demo给大家讲解一下,我的出发点不是如何实现,而是为什么?

首先我们来看看我么的code吧:

control:

    public class Users
    {
        public int UserID { set; get; }
        public string UserName { set; get; }
        public string UserEmail { set; get; }
    }
    public class ValuesController : ApiController
    {
        private static List<Users> _userList;
        static ValuesController()
        {
            _userList = new List<Users>
       {
           new Users {UserID = 1, UserName = "zzl", UserEmail = "[email protected]"},
           new Users {UserID = 2, UserName = "Spiderman", UserEmail = "[email protected]"},
           new Users {UserID = 3, UserName = "Batman", UserEmail = "[email protected]"}
       };
        }
        /// <summary>
        /// User Data List
        /// </summary>

        /// <summary>
        /// 得到列表对象
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Users> Get()
        {
            return _userList;
        }

        /// <summary>
        /// 得到一个实体,根据主键
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public Users Get(int id)
        {
            return _userList.FirstOrDefault();// (i => i.UserID == id);
        }

        /// <summary>
        /// 添加
        /// </summary>
        /// <param name="form">表单对象,它是唯一的</param>
        /// <returns></returns>
        public Users Post([FromBody] Users entity)
        {
            entity.UserID = _userList.Max(x => x.UserID) + 1;
            _userList.Add(entity);
            return entity;
        }

        /// <summary>
        /// 更新
        /// </summary>
        /// <param name="id">主键</param>
        /// <param name="form">表单对象,它是唯一的</param>
        /// <returns></returns>
        public Users Put(int id, [FromBody]Users entity)
        {
            var user = _userList.FirstOrDefault(i => i.UserID == id);
            if (user != null)
            {
                user.UserName = entity.UserName;
                user.UserEmail = entity.UserEmail;
            }
            else
            {

                _userList.Add(entity);
            }
            return user;
        }
        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="id">主键</param>
        /// <returns></returns>
        public void Delete(int id)
        {
            //_userList.Remove(_userList.FirstOrDefault(i => i.UserID == id));
            _userList.Remove(_userList.FirstOrDefault());
        }
        public string Options()
        {
            return null; // HTTP 200 response with empty body
        }

    }

HTML:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>web api test</title>
</head>
<body>
    <script type="text/javascript" src="js/jquery-1.7.1.js"></script>
<script type="text/javascript">

    function add() {
        $.ajax({
            url: "http://localhost:6221/api/values/",
            type: "POST",
            data: { "UserID": 4, "UserName": "test", "UserEmail": "[email protected]" },
            success: function (data) { alert(JSON.stringify(data)); }

        });
    }

    //更新
    function update(id) {
        $.ajax({
            url: "http://localhost:6221/api/values?id=" + id,
            type: "Put",
            data: { "UserID": 1, "UserName": "moditest", "UserEmail": "[email protected]" },
            success: function (data) { alert(JSON.stringify(data)); }
        });

    }
    function deletes(id) {
        $.ajax({
            url: "http://localhost:6221/api/values/1",
            type: "DELETE",
            success: function (data) { alert(data); }
        });
    }
    function users() {
        $.getJSON("http://localhost:6221/api/values", function (data) {
            alert(JSON.stringify(data));
        });
    }
    function user() {
        $.getJSON("http://localhost:6221/api/values/1", function (data) {
            alert(JSON.stringify(data));
        });
    }
</script>
<fieldset>
    <legend>测试Web Api
    </legend>
    <a href="javascript:add()">添加(post)</a>
    <a href="javascript:update(1)">更新(put)</a>
    <a href="javascript:deletes(1)">删除(delete)</a>
    <a href="javascript:users()">列表(Gets)</a>
    <a href="javascript:user()">实体(Get)</a>
</fieldset>

</body>
</html>

WebAPI的配置:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
      </customHeaders>
    </httpProtocol>

首先说明一下,配置中的httpProtocol和control中的Options都是在跨域的时候才需要的。

问题1.

Get,Post,Put,Delete,Options 这几个方法 ,服务器端是怎么来定位的, 或者说服务器是如何确定是调用哪个Action?

其实我以前的文章 Asp.net web Api源码分析-HttpActionDescriptor的创建 中有提到,这里简单回忆一下:

首先我们客户端的请求Url中都是 http://localhost:6221/api/values/ 打头,这里的values就是我们的Control,这样我们就可以很容易找到这个control下面的方法。主要的类是ApiControllerActionSelector,在它里面有一个子类ActionSelectorCacheItem, 其构造函数就负责初始化control里面的ReflectedHttpActionDescriptor,

MethodInfo[] allMethods = _controllerDescriptor.ControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
MethodInfo[] validMethods = Array.FindAll(allMethods, IsValidActionMethod);

_actionDescriptors = new ReflectedHttpActionDescriptor[validMethods.Length];
for (int i = 0; i < validMethods.Length; i++)
{
MethodInfo method = validMethods[i];
ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(_controllerDescriptor, method);
_actionDescriptors[i] = actionDescriptor;
HttpActionBinding actionBinding = actionDescriptor.ActionBinding;

// Building an action parameter name mapping to compare against the URI parameters coming from the request. Here we only take into account required parameters that are simple types and come from URI.
_actionParameterNames.Add(
actionDescriptor,
actionBinding.ParameterBindings
.Where(binding => !binding.Descriptor.IsOptional && TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType) && binding.WillReadUri())
.Select(binding => binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName).ToArray());
}

_actionNameMapping = _actionDescriptors.ToLookup(actionDesc => actionDesc.ActionName, StringComparer.OrdinalIgnoreCase);

int len = _cacheListVerbKinds.Length;
_cacheListVerbs = new ReflectedHttpActionDescriptor[len][];
for (int i = 0; i < len; i++)
{
_cacheListVerbs[i] = FindActionsForVerbWorker(_cacheListVerbKinds[i]);
}

这里的validMethods 就是我们定义6个方法(2个Get,Post,Put,Delete,Options),在ReflectedHttpActionDescriptor里面的InitializeProperties 的实现如下:

private void InitializeProperties(MethodInfo methodInfo)
{
_methodInfo = methodInfo;
_returnType = GetReturnType(methodInfo);
_actionExecutor = new Lazy<ActionExecutor>(() => InitializeActionExecutor(_methodInfo));
_attrCached = _methodInfo.GetCustomAttributes(inherit: true);
CacheAttrsIActionMethodSelector = _attrCached.OfType<IActionMethodSelector>().ToArray();
_actionName = GetActionName(_methodInfo, _attrCached);
_supportedHttpMethods = GetSupportedHttpMethods(_methodInfo, _attrCached);
}

private static Collection<HttpMethod> GetSupportedHttpMethods(MethodInfo methodInfo, object[] actionAttributes)
        {
            Collection<HttpMethod> supportedHttpMethods = new Collection<HttpMethod>();
            ICollection<IActionHttpMethodProvider> httpMethodProviders = TypeHelper.OfType<IActionHttpMethodProvider>(actionAttributes);
            if (httpMethodProviders.Count > 0)
            {
                // Get HttpMethod from attributes
                foreach (IActionHttpMethodProvider httpMethodSelector in httpMethodProviders)
                {
                    foreach (HttpMethod httpMethod in httpMethodSelector.HttpMethods)
                    {
                        supportedHttpMethods.Add(httpMethod);
                    }
                }
            }
            else
            {
                // Get HttpMethod from method name convention
                for (int i = 0; i < _supportedHttpMethodsByConvention.Length; i++)
                {
                    if (methodInfo.Name.StartsWith(_supportedHttpMethodsByConvention[i].Method, StringComparison.OrdinalIgnoreCase))
                    {
                        supportedHttpMethods.Add(_supportedHttpMethodsByConvention[i]);
                        break;
                    }
                }
            }

            if (supportedHttpMethods.Count == 0)
            {
                // Use POST as the default HttpMethod
                supportedHttpMethods.Add(HttpMethod.Post);
            }

            return supportedHttpMethods;
        }
 private static readonly HttpMethod[] _supportedHttpMethodsByConvention =
        {
            HttpMethod.Get,
            HttpMethod.Post,
            HttpMethod.Put,
            HttpMethod.Delete,
            HttpMethod.Head,
            HttpMethod.Options,
            new HttpMethod("PATCH")
        };

GetSupportedHttpMethods判断当前action支持的请求类型,首先读取HttpMethod attributes,如果没有我们就读取action的name(Get,Post,Put,Delete,Options),所以put 方法支持put httpmethod。实在没有httpmethod就添加默认的post。

现在我们来看看_cacheListVerbs里面放的是什么东西?

private readonly HttpMethod[] _cacheListVerbKinds = new HttpMethod[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post };

private ReflectedHttpActionDescriptor[] FindActionsForVerbWorker(HttpMethod verb)
{
List<ReflectedHttpActionDescriptor> listMethods = new List<ReflectedHttpActionDescriptor>();

foreach (ReflectedHttpActionDescriptor descriptor in _actionDescriptors)
{
if (descriptor.SupportedHttpMethods.Contains(verb))
{
listMethods.Add(descriptor);
}
}

return listMethods.ToArray();
}

到这里么知道_cacheListVerbs里面放的就是Get,Put,Post对应的action,方便后面通过http request type来查找action。

现在action list已经准备好了,然后确定该调用哪个了?在ActionSelectorCacheItem类里面有SelectAction。主要逻辑如下:

string actionName;
bool useActionName = controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName);

ReflectedHttpActionDescriptor[] actionsFoundByHttpMethods;

HttpMethod incomingMethod = controllerContext.Request.Method;

// First get an initial candidate list.
if (useActionName)
{
.......................................
}
else
{
// No {action} parameter, infer it from the verb.
actionsFoundByHttpMethods = FindActionsForVerb(incomingMethod);
}

// Throws HttpResponseException with MethodNotAllowed status because no action matches the Http Method
if (actionsFoundByHttpMethods.Length == 0)
{
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(
HttpStatusCode.MethodNotAllowed,
Error.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, incomingMethod)));
}

// Make sure the action parameter matches the route and query parameters. Overload resolution logic is applied when needed.
IEnumerable<ReflectedHttpActionDescriptor> actionsFoundByParams = FindActionUsingRouteAndQueryParameters(controllerContext, actionsFoundByHttpMethods, useActionName);

首先从路由里面获取actionname,restful请求地址都不含有actionname, 那么就从请求type里面获取action了,即这里的FindActionsForVerb方法,该方法首先从_cacheListVerbs里面找,如果没有找到再在当前control的所有action里面找,比如Delete,Options在_cacheListVerbs是没有的。 如果通过FindActionsForVerb找到的action是多个,那么久需要通过FindActionUsingRouteAndQueryParameters方法来过滤了,该方法首先读取route和query参数,查看是否满足action需要的参数。

如这里的get action,如果请求地址是http://localhost:6221/api/values 这个,那么Get(int id)肯定要被过滤掉,因为它需要参数id,但是这里没有参数id,所以只能返回Get() 了。如果地址http://localhost:6221/api/values/1的话,那么这里的2个action都满足条件 ,我们就取参数多的那个action。

if (actionsFound.Count() > 1)
{
// select the results that match the most number of required parameters
actionsFound = actionsFound
.GroupBy(descriptor => _actionParameterNames[descriptor].Length)
.OrderByDescending(g => g.Key)
.First();
}

到这里大家就应该知道后台是如何获取action的了吧。一句话,把Request.Method作为actionname。

2.浏览器跨域问题。

其实网上已经有很多说明:

详解XMLHttpRequest的跨域资源共享

Angular通过CORS实现跨域方案

利用CORS实现跨域请求

在网上找的这张图,并不是所有的跨域请求 都有Options预请求,简单跨域是不需要。

一个简单的请求应该满足如下要求:

1.请求方法为GET,POST 这里是否包含HEAD我不怎么清楚,没测试过,还有HEAD我实际也没有用到
2.请求方法中没有设置请求头(Accept, Accept-Language, Content-Language, Content-Type除外)如果设置了Content-Type头,其值为application/x-www-form-urlencoded, multipart/form-data或 text/plain

常用的复杂请求是:发送PUTDELETE等HTTP动作,或者发送Content-Type: application/json的内容,来看看预请求的请求头和返回头:

  • Access-Control-Allow-Origin(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写“*”。
  • Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
  • Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。
  • Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentialstrue时该项也为truewithCredentialsfalse时,省略该项不写。反之则导致请求失败。
  • Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。

一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。

Credentials

在跨域请求中,默认情况下,HTTP Authentication信息,Cookie头以及用户的SSL证书无论在预检请求中或是在实际请求都是不会被发送的。但是,通过设置XMLHttpRequest的credentials为true,就会启用认证信息机制。

虽然简单请求还是不需要发送预检请求,但是此时判断请求是否成功需要额外判断Access-Control-Allow-Credentials,如果Access-Control-Allow-Credentials为false,请求失败。

十分需要注意的的一点就是此时Access-Control-Allow-Origin不能为通配符"*"(真是便宜了一帮偷懒的程序员),如果Access-Control-Allow-Origin是通配符"*"的话,仍将认为请求失败

即便是失败的请求,如果返回头中有Set-Cookie的头,浏览器还是会照常设置Cookie。

有不当之处欢迎拍砖。

时间: 2024-11-03 10:16:12

WebAPi Restful 的实现和跨域的相关文章

Taurus.MVC 2.2 开源发布:WebAPI 功能增强(请求跨域及Json转换)

背景: 1:有用户反馈了关于跨域请求的问题. 2:有用户反馈了参数获取的问题. 3:JsonHelper的增强. 在综合上面的条件下,有了2.2版本的更新,也因此写了此文. 开源地址: https://github.com/cyq1162/taurus.mvc 下面对增强的功能进行介绍: 1:跨域请求 除了常规的的JsonP跨域,Html5开始支持增强跨域,则变得更为方便,只需要服务端请求头输出: 1  if (context.Request.UrlReferrer != null && 

.net core3.1 webapi + vue.js + axios实现跨域

我所要讲述的是,基于.net core3.1环境下的webapi项目,如何去使用axios对接前端的vue项目 既然谈到axios,这里贴出axios的官方文档地址: http://www.axios-js.com/zh-cn/docs/ 首先是前端部分进行设置 第一步,在vue项目中安装axios 在vs code的终端中输入命令 npm install axios 稍等片刻,即可完成安装 第二步,构建axios的测试api 首先需要在main.js中,引入前面安装的axios 然后在某个组件

Restful based service 的跨域调用

1.关于跨域, w3c的官方文档:https://www.w3.org/TR/cors/ 2.有时间再整理吧. <html> <head> <script src="./jquery-1.11.2.min.js"></script> </head> <body> <script> window.onload= function(){ /** * * Base64 encode / decode * *

让webapi支持CORS,可以跨域访问

1.在NuGet里搜索webapi找到下面的扩展,添加进项目里. 2.在Global.asax中添加一行代码 protected void Application_Start() { //添加CORS的支持 GlobalConfiguration.Configuration.EnableCors(); //其他东西 AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuratio

ASP.Net WebAPI与Ajax进行跨域数据交互时Cookies数据的传递

前言 最近公司项目进行架构调整,由原来的三层架构改进升级到微服务架构(准确的说是服务化,还没完全做到微的程度,颗粒度没那么细),遵循RESTFull规范,使前后端完全分离,实现大前端思想.由于是初次尝试,中途也遇到了不少问题.今天就来讨论一下其中之一的问题,WebAPI与前端Ajax 进行跨域数据交互时,由于都在不同的二级域名下(一级域名相同),导致Cookies数据无法获取. 最开始通过头部(Header)将Cookies传输到其WebAPI,也能解决问题. 下面讲述另外一种解决方案. 解决过

ASP.Net中关于WebAPI与Ajax进行跨域数据交互时Cookies数据的传递

本文主要介绍了ASP.Net WebAPI与Ajax进行跨域数据交互时Cookies数据传递的相关知识.具有很好的参考价值.下面跟着小编一起来看下吧 前言 最近公司项目进行架构调整,由原来的三层架构改进升级到微服务架构(准确的说是服务化,还没完全做到微的程度,颗粒度没那么细),遵循RESTFull规范,使前后端完全分离,实现大前端思想.由于是初次尝试,中途也遇到了不少问题.今天就来讨论一下其中之一的问题,WebAPI与前端Ajax 进行跨域数据交互时,由于都在不同的二级域名下(一级域名相同),导

关于AJAX跨域调用ASP.NET MVC或者WebAPI服务的问题及解决方案

原文:http://www.cnblogs.com/chenxizhang/p/3821703.html 问题描述 当跨域(cross domain)调用ASP.NET MVC或者ASP.NET Web API编写的服务时,会发生无法访问的情况. 重现方式 1.使用模板创建一个最简单的ASP.NET Web API项目,调试起来确认能正常工作 public class TestController : ApiController { // GET api/test public IEnumera

跨域学习笔记1--跨域调用webapi

在做Web开发中,常常会遇到跨域的问题,到目前为止,已经有非常多的跨域解决方案. 通过自己的研究以及在网上看了一些大神的博客,写了一个Demo 首先新建一个webapi的程序,如下图所示: 由于微软已经给我们搭建好了webapi的环境,所以我们不必去添加引用一些dll,直接开始写代码吧. 因为这只是做一个简单的Demo,并没有连接数据库. 第一步我们要在Models文件夹里添加一个实体类Employees,用来存放数据. Employees.cs里的内容如下: 1 using System; 2

跨域调用webapi

web端跨域调用webapi 在做Web开发中,常常会遇到跨域的问题,到目前为止,已经有非常多的跨域解决方案. 通过自己的研究以及在网上看了一些大神的博客,写了一个Demo 首先新建一个webapi的程序,如下图所示: 由于微软已经给我们搭建好了webapi的环境,所以我们不必去添加引用一些dll,直接开始写代码吧. 因为这只是做一个简单的Demo,并没有连接数据库. 第一步我们要在Models文件夹里添加一个实体类Employees,用来存放数据. Employees.cs里的内容如下: 1