1. 程式人生 > >WebAPI 用ActionFilterAttribute實現token令牌驗證與對Action的許可權控制

WebAPI 用ActionFilterAttribute實現token令牌驗證與對Action的許可權控制

先說說使用者身份的識別,簡單的做了一個token機制。使用者登入,後臺產生令牌,發放令牌,使用者攜帶令牌訪問...1.cache管理類,由於博主使用的HttpRuntime.Cache來儲存token,IIS重啟或者意外關閉等情況會造成cache清空,只好在資料庫做了cache的備份,在cache為空的時候查詢資料庫是否有cache資料,有則是cache被意外清空,需要重新放在cache中。複製程式碼複製程式碼 /// /// 快取管理 /// 將令牌、使用者憑證以及過期時間的關係資料存放於Cache中 /// public class CacheManager { private static readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService(); /// /// 初始化快取資料結構 /// /// token 令牌 /// uuid 使用者ID憑證 /// userType 使用者類別 /// timeout 過期時間 /// /// private static void CacheInit() { if (HttpRuntime.Cache["PASSPORT.TOKEN"] == null) { DataTable dt = new DataTable(); dt.Columns.Add("token", Type.GetType("System.String")); dt.Columns["token"].Unique = true; dt.Columns.Add("uuid", Type.GetType("System.Object")); dt.Columns["uuid"].DefaultValue = null; dt.Columns.Add("userType", Type.GetType("System.String")); dt.Columns["userType"].DefaultValue = null; dt.Columns.Add("timeout", Type.GetType("System.DateTime")); dt.Columns["timeout"].DefaultValue = DateTime.Now.AddDays(7); DataColumn[] keys = new DataColumn[1]; keys[0] = dt.Columns["token"]; dt.PrimaryKey = keys; var tempCaches = tempCacheService.GetAllCaches(); if (tempCaches.Any()) { foreach (var tempCacheDTOShow in tempCaches) { DataRow dr = dt.NewRow(); dr["token"] = tempCacheDTOShow.UserToken; dr["uuid"] = tempCacheDTOShow.UserAccountId; dr["userType"] = tempCacheDTOShow.UserType.ToString(); dr["timeout"] = tempCacheDTOShow.EndTime; dt.Rows.Add(dr); } } //Cache的過期時間為 令牌過期時間*2 HttpRuntime.Cache.Insert("PASSPORT.TOKEN", dt, null, DateTime.MaxValue, TimeSpan.FromDays(7 * 2)); } }/// /// 獲取使用者UUID標識 /// /// /// public static Guid GetUUID(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { return new Guid(dr[0]["uuid"].ToString()); } return Guid.Empty; } /// /// 獲取使用者類別(分為員工、企業、客服、管理員等,後期做許可權驗證使用) /// /// /// public static string GetUserType(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { return dr[0]["userType"].ToString(); } return null; }/// /// 判斷令牌是否存在 /// /// 令牌 /// public static bool TokenIsExist(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { var timeout = DateTime.Parse(dr[0]["timeout"].ToString()); if (timeout > DateTime.Now) { return true; } else { RemoveToken(token); return false; } } return false; } /// /// 移除某令牌 /// /// /// public static bool RemoveToken(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { dt.Rows.Remove(dr[0]); } return true; } /// /// 更新令牌過期時間 /// /// 令牌 /// 過期時間 public static void TokenTimeUpdate(string token, DateTime time) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { dr[0]["timeout"] = time; } } /// /// 新增令牌 /// /// 令牌 /// 使用者ID憑證 /// 使用者類別 /// 過期時間 public static void TokenInsert(string token, object uuid, string userType, DateTime timeout) { CacheInit(); // token不存在則新增 if (!TokenIsExist(token)) { DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow dr = dt.NewRow(); dr["token"] = token; dr["uuid"] = uuid; dr["userType"] = userType; dr["timeout"] = timeout; dt.Rows.Add(dr); HttpRuntime.Cache["PASSPORT.TOKEN"] = dt; tempCacheService.Add_TempCaches(new List() { new TempCacheDTO_ADD() { EndTime = timeout, UserAccountId = new Guid(uuid.ToString()), UserToken = new Guid(token), UserType = (UserType)Enum.Parse(typeof(UserType),userType) } }); } // token存在則更新過期時間 else { TokenTimeUpdate(token, timeout); tempCacheService.Update_TempCaches(new Guid(token), timeout); } } }複製程式碼複製程式碼2.接下來就是對使用者攜帶的token進行驗證了,通過繼承ActionFilterAttribute來實現,在這裡還需要考慮到匿名訪問API,對於部分API,是允許匿名訪問(不登入訪問)的。所以,先寫一個代表匿名的Attribute:複製程式碼複製程式碼 /// /// 匿名訪問標記 /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AnonymousAttribute : Attribute { }複製程式碼複製程式碼然後給允許匿名訪問的Action打上[Anonymous]標籤就OK,再來看我們的token驗證程式碼:複製程式碼複製程式碼 /// /// 使用者令牌驗證/// public class TokenProjectorAttribute : ActionFilterAttribute { private const string UserToken = "token"; private readonly IAccountInfoService accountInfoService = ServiceLocator.Instance.GetService(); private readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService(); public override void OnActionExecuting(HttpActionContext actionContext) { // 匿名訪問驗證 var anonymousAction = actionContext.ActionDescriptor.GetCustomAttributes(); if (!anonymousAction.Any()) { // 驗證token var token = TokenVerification(actionContext); } base.OnActionExecuting(actionContext); } /// /// 身份令牌驗證 /// /// protected virtual string TokenVerification(HttpActionContext actionContext) { // 獲取token var token = GetToken(actionContext.ActionArguments, actionContext.Request.Method); // 判斷token是否有效 if (!CacheManager.TokenIsExist(token)) { throw new UserLoginException("Token已失效,請重新登陸!"); } // 判斷使用者是否被凍結 if (accountInfoService.Exist_User_IsForzen(AccountHelper.GetUUID(token))) { CacheManager.RemoveToken(token); tempCacheService.Delete_OneTempCaches(new Guid(token)); throw new UserLoginException("此使用者已被凍結,請聯絡客服!"); } return token; } private string GetToken(Dictionary actionArguments, HttpMethod type) { var token = ""; if (type == HttpMethod.Post) { foreach (var value in actionArguments.Values) { token = value.GetType().GetProperty(UserToken) == null ? GetToken(actionArguments, HttpMethod.Get) : value.GetType().GetProperty(UserToken).GetValue(value).ToString(); } } else if (type == HttpMethod.Get) { if (!actionArguments.ContainsKey(UserToken)) { throw new Exception("未附帶token!"); } if (actionArguments[UserToken] != null) { token = actionArguments[UserToken].ToString(); } else { throw new Exception("token不能為空!"); } } else { throw new Exception("暫未開放其它訪問方式!"); } return token; } }複製程式碼複製程式碼這裡對GetToken方法做一下解釋:1.博主只做了POST與GET方法的驗證,其他請求未使用也就沒做,歡迎大家補充2.POST方式裡面的回撥是解決POST請求介面只有一個簡單引數的情況,例如下面的介面:複製程式碼複製程式碼 /// /// 手動向新使用者推送簡訊 /// /// [HttpPost] [Route("api/Common/PushNewUserSMS")]public PushNewWorkSMSResult PushNewUserSMS([FromBody]string token) { sendMessagesService.PushNewUserSMS(); return new PushNewWorkSMSResult() { Code = 0 }; }複製程式碼複製程式碼當然,POST方式一般都會把引數寫進一個類裡,對於一個引數的情況,博主不喜歡那麼幹。這麼寫,需要AJAX提交時空變數名才能獲取:複製程式碼複製程式碼 // 推送新使用者營銷簡訊 function pushNewUserSMS() { $(".tuiguang").unbind("click"); // 注意下面的引數為空“” $.post(Config.Api.Common.PushNewUserSMS, { "": $.cookie("MPCBtoken") }, function (data) { if (data.Code == 0) { alert("傳送成功!"); _initDatas(); } else { Config.Method.JudgeCode(data, 1); } }); }複製程式碼複製程式碼這樣,我們就只需要在每個controller上打上[TokenProjector]標籤,再在允許匿名的Action上打上[Anonymous]標籤就能輕鬆的搞定token驗證了。3.除了token驗證外呢,我門還想對Action進行使用者角色的控制,比如一個獲取登入使用者錢包餘額的Action(A),肯定只有員工、企業才能訪問,管理員、客服沒有錢包,所以不允許訪問,從業務上應該是去訪問另外一個獲取指定使用者錢包餘額的Action(B),當然這個Action又不能對員工、企業開放許可權。這就涉及到需要實現一個控制Action訪問許可權的功能,上面我們對使用者的token進行了驗證,那麼拿到token就拿到了使用者基本資訊(包括角色),那就只需要做一個對Action的許可權標註就能解決問題了,我們先寫一個代表權限控制的Attribute:複製程式碼複製程式碼 /// /// 許可權控制標記 /// [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class ModuleAuthorizationAttribute : Attribute { public ModuleAuthorizationAttribute(params string[] authorization) { this.Authorizations = authorization; } /// /// 允許訪問角色 /// public string[] Authorizations { get; set; } }複製程式碼複製程式碼在每個需要許可權控制的Action上打上[ModuleAuthorization]標籤,並註明訪問角色:複製程式碼複製程式碼 /// /// 獲取錢包餘額 /// /// /// [HttpGet] [Route("api/Account/GetWalletBalance")] [ModuleAuthorization(new[] { "Staff", "Enterprise" })] public GetWalletBalanceResult GetWalletBalance(string token) { var result = this.walletService.Get_Wallet_Balance(AccountHelper.GetUUID(token)); return new GetWalletBalanceResult() { Code = 0, Balance = result }; } /// /// 管理員 /// 處理提現申請 /// /// /// [HttpPost] [Route("api/Account/HandleTempWithdrawals")] [ModuleAuthorization(new[] { "PlatformCustomer" })] public HandleTempWithdrawalsResult HandleTempWithdrawals( [FromBody] HandleTempWithdrawalsModel handleTempWithdrawalsModel) { walletService.Handle_TempWithdrawals(AccountHelper.GetUUID(handleTempWithdrawalsModel.token), handleTempWithdrawalsModel.message, handleTempWithdrawalsModel.tempID, handleTempWithdrawalsModel.isSuccess); return new HandleTempWithdrawalsResult() { Code = 0 }; }複製程式碼複製程式碼然後我們修改TokenProjectorAttribute這個類,在驗證token後做許可權驗證,許可權驗證方法如下:複製程式碼複製程式碼 /// /// Action 訪問許可權驗證 /// /// 身份令牌 /// /// protected virtual void AuthorizeCore(string token, HttpActionContext actionContext) { // 許可權控制Action驗證 var moduleAuthorizationAction = actionContext.ActionDescriptor.GetCustomAttributes(); if (moduleAuthorizationAction.Any()) { var userRole = AccountHelper.GetUserType(token); if (!moduleAuthorizationAction[0].Authorizations.Contains(userRole.ToString())) { throw new Exception("使用者非法跨許可權訪問,token:" + token); } } }複製程式碼複製程式碼 OK,終於實現了webAPI對使用者令牌與Action許可權的驗證。