1. 程式人生 > >Asp.Net WebApi一個簡單的Token驗證

Asp.Net WebApi一個簡單的Token驗證

1、前言:

WebAPI主要開放資料給手機APP,Pad,其他需要得知資料的系統,或者軟體應用。Web 使用者的身份驗證,及頁面操作許可權驗證是B/S系統的基礎功能。我上次寫的《Asp.Net MVC WebAPI的建立與前臺Jquery ajax後臺HttpClient呼叫詳解》這種跟明顯安全性不是那麼好,於是乎這個就來了 ,使用者需要訪問的API都必須帶有票據過來,說白了就是登陸之後含有使用者資訊的Token。開始擼...

2、新建一個WebApi專案,在App_Start資料夾下面新建一個BaseApiController控制器,這是基礎的Api控制器,後面有要驗證的介面都繼承這個控制器:

using LoginReqToken.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;

namespace LoginReqToken.App_Start
{
    /// <summary>
    /// 基礎Api控制器  所有的都繼承他
    /// </summary>
    public class BaseApiController : ApiController
    {
       
       /// <summary>
       /// 建構函式賦值
       /// </summary>
        public BaseApiController()
        {
            TokenValue = HttpContext.Current.Session[LoginID] ?? "";
            HttpContext.Current.Request.Headers.Add("TokenValue", TokenValue.ToString());
        }
        /// <summary>
        /// 資料庫上下文
        /// </summary>
        public WYDBContext db = WYDBContextFactory.GetDbContext();
        /// <summary>
        /// token值 登入後賦值請求api的時候新增到header中
        /// </summary>
        public static object TokenValue { get; set; } = "";
        /// <summary>
        /// 登入者賬號
        /// </summary>
        public static string LoginID { get; set; } = "";
    }
}

這個建構函式裡主動加一個header頭資訊 ,因為每次訪問的時候都要執行建構函式,在那邊驗證的時候都要從Header中取出來,計算出使用者名稱 是否跟Session快取的一致這樣判斷的

3、在建一個TokenCheckFilter.cs繼承AuthorizeAttribute重寫基類的驗證方式,重寫HandleUnauthorizedRequest

using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Security;
namespace LoginReqToken.App_Start
{
    /// <summary>
    /// token驗證
    /// </summary>
    public class TokenCheckFilter: AuthorizeAttribute
    {

        /// <summary>
        /// 重寫基類的驗證方式,加入自定義的Ticket驗證
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var content = actionContext.Request.Properties["MS_HttpContext"] as HttpContextBase;
            //獲取token(請求頭裡面的值)
            var token = HttpContext.Current.Request.Headers["TokenValue"] ?? "";
            //是否為空
            if (!string.IsNullOrEmpty(token.ToString()))
            {
                //解密使用者ticket,並校驗使用者名稱密碼是否匹配
                if (ValidateTicket(token.ToString()))
                    base.IsAuthorized(actionContext);
                else
                    HandleUnauthorizedRequest(actionContext);
            }
            //如果取不到身份驗證資訊,並且不允許匿名訪問,則返回未驗證403
            else
            {
                var attributes = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>();
                bool isAnonymous = attributes.Any(a => a is AllowAnonymousAttribute);
                if (isAnonymous) base.OnAuthorization(actionContext);
                else HandleUnauthorizedRequest(actionContext);
            }
        }

        //校驗使用者名稱密碼(對Session匹配,或資料庫資料匹配)
        private bool ValidateTicket(string encryptToken)
        {
            //解密Ticket
            var strTicket = FormsAuthentication.Decrypt(encryptToken).UserData;
            //從Ticket裡面獲取使用者名稱和密碼
            var index = strTicket.IndexOf("&");
            string userName = strTicket.Substring(0, index);
            string password = strTicket.Substring(index + 1);
            //取得session,不通過說明使用者退出,或者session已經過期
            var token = HttpContext.Current.Session[userName];
            if (token == null)
                return false;
            //對比session中的令牌
            if (token.ToString() == encryptToken)
                return true;
            return false;
        }
        /// <summary>
        /// 重寫HandleUnauthorizedRequest
        /// </summary>
        /// <param name="filterContext"></param>
        protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
        {
            base.HandleUnauthorizedRequest(filterContext);

            var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage();
            //狀態碼401改為其他狀態碼來避免被重定向。最合理的是改為403,表示伺服器拒絕。
            response.StatusCode = HttpStatusCode.Forbidden;
            var content = new 
            {
                success = false,
                errs = new[] { "服務端拒絕訪問:你沒有許可權?,或者掉線了?" }
            };
            response.Content = new StringContent(Json.Encode(content), Encoding.UTF8, "application/json");
        }

    }
}

4、在WebApiConfig.cs配置檔案裡面修改一下路由加上/{action},這樣就能呼叫到具體的哪一個了

 

 

 

Webapi預設是不支援Session的,所以我們需要在Global載入時候新增對Session的支援,在Global.asax裡面重寫Application_PostAuthorizeRequest,不然執行呼叫會直接異常

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
        /// <summary>
        /// 重寫Application_PostAuthorizeRequest
        /// </summary>
        protected void Application_PostAuthorizeRequest()
        {
            //對Session的支援,不然執行呼叫會直接異常
            HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
        }
    }

 

 

5、現在來寫一個登陸,新建一個控制器LoginController繼承BaseApiController 裡面寫一個登陸的方法Login 登陸頁面就直接在Home的index裡面寫一個簡單的就行了這個控制器訪問就不受限制了加上註解AllowAnonymous

[AllowAnonymous]
    public class LoginController : BaseApiController
    {
        [HttpGet]
        public object Login(string uName, string uPassword)
        {
            var user = db.Users.Where(x => x.LoginID == uName && x.Password == uPassword).FirstOrDefault();
            if (user==null)
            {
                return Json(new { ret = 0, data = "", msg = "使用者名稱密碼錯誤" });
            }
            FormsAuthenticationTicket token = new FormsAuthenticationTicket(0, uName, DateTime.Now, DateTime.Now.AddHours(12), true, $"{uName}&{uPassword}", FormsAuthentication.FormsCookiePath);
            //返回登入結果、使用者資訊、使用者驗證票據資訊
            var _token = FormsAuthentication.Encrypt(token);
            //將身份資訊儲存在session中,驗證當前請求是否是有效請求
            LoginID = uName;
            TokenValue = _token;
            HttpContext.Current.Session[LoginID] = _token;
            return Json(new { ret = 1, data = _token, msg = "登入成功!" });
        }
    }

 

登陸頁面 簡單而粗暴

 

<br /><br />
<input type="text" name="txtLoginID" id="txtLoginID"  />
<br /><br />
<input type="password" name="txtPassword" id="txtPassword"  />
<br /><br />
<input type="button" id="btnSave" value="登入驗證" />
<script type="text/javascript" src="~/Scripts/jquery-3.3.1.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        $("#btnSave").click(function () {
            $.ajax({
                type: "GET",
                url: "/Api/Login/Login",
                dataType: "json",
                data: { "uName": $("#txtLoginID").val(), "uPassword": $("#txtPassword").val()},
                success: function (data) {
                    if (data.ret > 0) {
                        alert(data.msg+"Token:  "+data.data);
                    }
                    else {
                        alert(data.msg);
                    }
                },
                error: function (ret) {
                    console.log(ret);
                }
            });
        });
    });
</script>

 

 

 

 登陸這個我是寫了連結資料庫的自己練習可以最易更改一個固定的值 現在應該可以看到返回的Token資料了

 

 

 

 6、現在就可以寫Api了 都繼承BaseApiController這個控制器的方法上面需要驗證的都要加上驗證的註解,我是整個控制都要就直接寫在類上面了,隨便寫一個舉舉例子,就比如全國省市縣的查詢

using LoginReqToken.App_Start;
using LoginReqToken.Models;
using LoginReqToken.Models.DTO;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace LoginReqToken.Controllers
{

    /// <summary>
    /// 區域查詢
    /// </summary>
    [TokenCheckFilter]
    public class AreaOpController : BaseApiController
    {
        /// <summary>
        /// 獲取全部區域
        /// </summary>
        /// <returns></returns>
        public Result GetAllAreas()
        {
            var data = db.AddressAll.OrderBy(x => x.ID);
            if(data.Count()>0)
            {
                var ret = new Result()
                {
                    Ret = 1,
                    Code = "200",
                    Msg = "獲取資料成功",
                    Data = JsonConvert.SerializeObject(data)

                };
                return ret;
            }
            else
            {
                var ret = new Result()
                {
                    Ret = 0,
                    Code = "400",
                    Msg = "介面失敗異常",
                    Data = ""

                };
                return ret;
            }
        }
        /// <summary>
        /// 查詢某個省市直轄市自治區下所有的資訊
        /// </summary>
        /// <param name="name">省名稱(全名)</param>
        /// <returns></returns>
        public Result GetProvinceByName(string name)
        {
            var codeID = db.AddressAll.FirstOrDefault(x => x.Name == name)?.ID;
            if(codeID<=0)
            {
                var ret = new Result()
                {
                    Ret = 1,
                    Code = "F",
                    Msg = "沒有查到相關資料",
                    Data = ""

                };
                return ret;
            }
            var bb = db.AddressAll.Where(x=>x.ID>0).AsEnumerable();
            var data = GetProvinceCity(bb,codeID).ToList();
            if (data.Count() > 0)
            {
                var ret = new Result()
                {
                    Ret = 1,
                    Code = "200",
                    Msg = "獲取資料成功",
                    Data = JsonConvert.SerializeObject(data)

                };
                return ret;
            }
            else
            {
                var ret = new Result()
                {
                    Ret = 0,
                    Code = "500",
                    Msg = "查詢不到資料或者介面調用出錯",
                    Data = ""

                };
                return ret;
            }

        }
        /// <summary>
        /// 遞迴獲取樹形資料
        /// </summary>
        /// <param name="areasDTOs"></param>
        /// <param name="parentID"></param>
        /// <returns></returns>
        public IEnumerable<object> GetProvinceCity(IEnumerable<AddressAll> areasDTOs,int? parentID)
        {
            var data = areasDTOs as AddressAll[] ?? areasDTOs.ToArray();
            var ret = data.Where(n => n.ParentID == parentID).Select(n => new
            {
                n.ID,
                n.Code,
                n.ParentID,
                n.Name,
                n.LevelNum,
                n.OrderNum,
                children = GetProvinceCity(data, n.ID)
            });
            return ret;
        }
    }
}

 

記錄一個EF隨意取資料庫條數資訊是這麼寫的 var data = db.CnblogsList.OrderBy(p => Guid.NewGuid()).Take(100);

現在看效果圖

 

 

 沒有登陸的時候是進不去的 postman上面的效果也看一下

 

 

效果都是一樣的,如果登入了就可以直接訪問 了 不用加引數 ,只有方法需要引數的就可以加 

 

 

 

 

 

 這裡貼一個呼叫的程式碼:

 HttpClient bb = new HttpClient();
            //獲取埠
            HttpContent httpContent = new StringContent("");
            httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            
             var dl = bb.GetAsync("http://localhost:63828/api/Login/login?uName=admin&uPassword=admin888").Result.Content.ReadAsStringAsync().Result;
            var token = JsonConvert.DeserializeObject<Result>(dl);
           
            for (var i=0;i<100;i++)
            {
                
                var ret = bb.GetAsync("http://localhost:63828/api/Cnblog/GetAllArtic").Result.Content.ReadAsStringAsync().Result;
            }

 

7、總結:

1)、總體思路,如果是合法的Http請求,在Http請求頭中會有使用者身份的票據資訊,服務端會讀取票據資訊,並校驗票據資訊是否完整有效,如果滿足校驗要求,則進行業務資料的處理,並返回給請求發起方;

2) 如果沒有票據資訊,或者票據資訊不是合法的,則返回“未授權的訪問”異常訊息給前端,由前端處理此異常。

3)、登入的時候判斷使用者名稱跟密碼對不對,對了就生成使用者資訊的Token,Session儲存一個Token,BaseApiController裡面的登入名跟Token也賦值了。儲存這些票據資訊。

4)、當用戶有許可權操作頁面或頁面元素時,跳轉到頁面,並由頁面Controller提交業務資料處理請求到api伺服器; 如果使用者沒有許可權訪問該頁面或頁面元素時,則顯示“未授權的訪問操作”,跳轉到系統異常處理頁面。

5)、 api業務服務處理業務邏輯,並將結果以Json 資料返回,返回渲染後的頁面給瀏覽器前端,並呈現業務資料到頁面;

8、測試地址:

http://www.yijianlan.com:8001/   ---------------------->先登入,使用者名稱 test密碼 123456 可以呼叫除錯的介面 然後訪問看看,其他的js 呼叫, 其他平臺的我沒有試過,還不知道問題,歡迎指教!

http://www.yijianlan.com:8001/api/AreaOp/GetProvinceByName?name=省全稱   -------->    檢視某個省市的所有子集

http://www.yijianlan.com:8001/api/AreaOp/GetAllAreas -------------------------------------------->  獲取全部區域(全國首位省市縣)

http://www.yijianlan.com:8001/api/Cnblog/GetAllArtic -----------------------------------------------> 獲取部落格園資料(這是以前爬蟲抓的有2年了吧),隨機一百條

http://www.yijianlan.com:8001/api/Cnblog/GetArticByName?name=標題 --------------------->  查詢資料按標題