Asp.net 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 = "bfyxzls@sina.com"},
           new Users {UserID = 2, UserName = "Spiderman", UserEmail = "Spiderman@cnblogs.com"},
           new Users {UserID = 3, UserName = "Batman", UserEmail = "Batman@cnblogs.com"}
       };
        }
        /// <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": "Parry@cnblogs.com" },
            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": "Parry@cnblogs.com" },
            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") 
        };
View Code

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。

有不當之處歡迎拍磚。下載地址 http://download.csdn.net/detail/dz45693/9486586


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM