在項目開發中,爲了安全、方便地判斷用戶是否有訪問當前資源(Action)的權限,咱們通常經過全局過濾器來實現。css
Asp.net MVC 頁面中常見的權限判斷使用過濾器主要在如下幾種狀況(根據權限判斷的前後順序):html
一、判斷要訪問的Controller或Action是能夠匿名訪問;數據庫
二、判斷要訪問的Controller或Action是否登陸就能夠訪問;json
三、判斷是否有訪問某個Controller的Action的權限;瀏覽器
四、資源訪問權限,如在下拉菜單中獲取當前帳號能夠顯示幾個下拉項;安全
經過在全局過濾器中逐項判斷,咱們可讓有權限的用戶順利經過,沒有權限的就會被禁止掉,下面來看看過濾器的代碼實現:服務器
public class CheckPermissionFilter : IAuthorizationFilter { /// <summary> /// 權限判斷 /// </summary> /// <param name="filterContext"></param> public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } if (filterContext.HttpContext.Request.Url == null) { throw new ArgumentNullException("filterContext"); } string pageUrl = filterContext.HttpContext.Request.Url.AbsolutePath; //OperateContext.GetThisPageUrl(false); // 是不是Ajax請求 var bAjax = filterContext.HttpContext.Request.IsAjaxRequest(); // 一、容許匿名訪問 用於標記在受權期間要跳過 AuthorizeAttribute 的控制器和操做的特性 var actionAnonymous = filterContext.ActionDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true) as IEnumerable<AllowAnonymousAttribute>; var controllerAnonymous = filterContext.Controller.GetType().GetCustomAttributes(typeof(AllowAnonymousAttribute), true) as IEnumerable<AllowAnonymousAttribute>; if ((actionAnonymous != null && actionAnonymous.Any()) || (controllerAnonymous != null && controllerAnonymous.Any())) { return; } // 判斷是否登陸或登陸已超時 須要從新登陸 if (Infrastructure.OperateContext.Current.UserInfo == null) { if (bAjax) { BusinessResultBase result = new BusinessResultBase(); result.Title = "未登陸或登陸已超時"; result.Status = false; result.StatusCode = BusinessStatusCode.LoginTimeOut.ToString(); result.StatusMessage = "請從新登陸系統。"; var jsonResult = new JsonResult(); jsonResult.Data = result; filterContext.Result = jsonResult; } else { filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Account", action = "Login" })); } } else { // 二、拒絕某個帳號登陸當前系統 if (OperateContext.Current.IsDenyVisit()) { if (bAjax) { BusinessResultBase result = new BusinessResultBase(); result.Title = "拒絕訪問當前系統"; result.Status = false; result.StatusCode = BusinessStatusCode.AccessDeny.ToString(); result.StatusMessage = "您的帳號不容許訪問當前系統。"; var jsonResult = new JsonResult(); jsonResult.Data = result; filterContext.Result = jsonResult; } else { filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Common", action = "DenyAccess", bAjaxReq = false, message = "沒有獲取您擁有的權限菜單,請嘗試從新登陸。" })); } } else { // 三、判斷登陸狀態 Controller Action 標籤 某些功能只需判斷是否登陸 用戶沒登陸 調到登陸頁面 // 判斷Controller上是否有CheckLoginAttribute標籤 只須要登陸就能夠訪問 var checkLoginControllerAttr = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(CheckLoginAttribute), true) as IEnumerable<CheckLoginAttribute>; if (checkLoginControllerAttr != null && checkLoginControllerAttr.Any()) { return; } // 判斷action上是否有CheckLoginAttribute標籤 只須要登陸就能夠訪問 var checkLoginActionAttr = filterContext.ActionDescriptor.GetCustomAttributes(typeof(CheckLoginAttribute), true) as IEnumerable<CheckLoginAttribute>; if (checkLoginActionAttr != null && checkLoginActionAttr.Any()) { return; } // 四、有些要判斷是否有某個菜單 action的權限 具體判斷某個用戶是否有某個權限 // 用於標記在受權期間須要CustomerResourceAttribute 的操做的特性 var attNames = filterContext.ActionDescriptor.GetCustomAttributes(typeof(CustomerResourceAttribute), true) as IEnumerable<CustomerResourceAttribute>; // 用戶具備的菜單 var moduleList = OperateContext.Current.GetPermissionList(); if (moduleList == null || !moduleList.Any()) { if (bAjax) { BusinessResultBase result = new BusinessResultBase(); result.Title = "沒有訪問權限"; result.Status = false; result.StatusCode = BusinessStatusCode.AccessDeny.ToString(); result.StatusMessage = "沒有獲取您擁有的權限菜單,請嘗試從新登陸。"; var jsonResult = new JsonResult(); jsonResult.Data = result; filterContext.Result = jsonResult; } else { filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Common", action = "DenyAccess", bAjaxReq = false, message = "沒有獲取您擁有的權限菜單,請嘗試從新登陸。" })); } } else { // 判斷用戶的權限菜單中的code是否與控制器上標示的資源的code一致 var joinResult = (from aclEntity in moduleList join attName in attNames on aclEntity.Code equals attName.ResourceName select attName).Any(); if (!joinResult) { if (bAjax) { BusinessResultBase result = new BusinessResultBase(); result.Title = "沒有訪問權限"; result.Status = false; result.StatusCode = BusinessStatusCode.AccessDeny.ToString(); result.StatusMessage = "您沒有訪問權限:" + pageUrl; var jsonResult = new JsonResult(); jsonResult.Data = result; filterContext.Result = jsonResult; } else { filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Common", action = "DenyAccess", bAjaxReq = false, message = "您沒有訪問權限:" + pageUrl })); } } else { return; } } } } } }
基於這樣的過濾器,咱們的Controller和Action該怎麼寫呢?下面分別舉出幾個demoide
一、匿名訪問的Controller,這是一個測試安裝環境中數據庫鏈接是否連通的功能:測試
/// <summary> /// InstallationController /// /// 安裝環境測試 /// /// 2015-12-25 版本:1.0 SongBiao 建立文件。 /// /// <author> /// <name>SongBiao</name> /// <date>2015-12-25</date> /// </author> /// </summary> [AllowAnonymous] public class InstallationController : Controller { // // GET: /InstallationEnvironment/ public ActionResult Index() { string result; try { // 獲取數據庫時間 測試數據庫是否通 using (IDbHelper dbHelper = DbHelperFactory.GetHelper(BaseSystemInfo.UserCenterDbType, BaseSystemInfo.UserCenterDbConnection)) { result ="數據庫已連通,當前數據庫時間:"+ DateTime.Parse(dbHelper.GetDbDateTime()).ToString(BaseSystemInfo.DateTimeFormat); } } catch (Exception ex) { result="數據庫鏈接異常:"+ex.Message; } ViewBag.Result = result; return View(); } }
這個頭部增長了AllowAnonymous標籤,實現匿名訪問功能,用戶不需登陸便可訪問。ui
二、只須要登陸就能夠訪問的功能
namespace DotNet.MVC.Controllers { using DotNet.Business; using DotNet.Model; using DotNet.MVC.Attributes; using DotNet.MVC.Infrastructure; /// <summary> /// 系統菜單權限 /// /// 修改紀錄 /// /// 2015-09-09 版本:1.0 SongBiao 建立文件。 /// /// <author> /// <name>SongBiao</name> /// <date>2015-09-09</date> /// </author> /// </summary> [CheckLogin] public class ItemsSystemController : BaseController { // // GET: /ItemsSystem/ public ActionResult Index() { return View(); } /// <summary> /// 返回所有子系統下拉列表數據 /// </summary> /// <returns></returns> public ActionResult GetSystemSelect() { string tableName = "ItemsSystem"; Hashtable hashTable = new Hashtable(); List<BaseItemDetailsEntity> list = null; var itemDetailsManager = new BaseItemDetailsManager(UserCenterDbHelper, UserInfo, tableName); // 管理員取得全部數據 if (UserInfo.IsAdministrator) { list= itemDetailsManager.GetList<BaseItemDetailsEntity>(new KeyValuePair<string, object>(BaseItemDetailsEntity.FieldDeletionStateCode, 0) , BaseItemDetailsEntity.FieldSortCode); } else { string permissionCode = "Resource.ManagePermission"; BasePermissionScopeManager permissionScopeManager = new BasePermissionScopeManager(UserCenterDbHelper, UserInfo); string[] ids = permissionScopeManager.GetResourceScopeIds(UserInfo.Id, tableName, permissionCode); list = itemDetailsManager.GetList<BaseItemDetailsEntity>(ids); } if (list != null && list.Any()) { var result = (from p in list select new { key = p.ItemName, value = p.ItemValue }).ToList(); hashTable.Add("list", result); } else { hashTable.Add("list", null); } return Json(hashTable, JsonRequestBehavior.AllowGet); } } }
這裏在控制器上使用 CheckLogin 標籤來實現只要用戶登陸就能夠訪問當前控制器裏的Action,固然也能夠指定Controller下某個Action只要登陸就能夠訪問,以下寫法
#region public ActionResult LogOff() 退出 /// <summary> /// 退出 /// </summary> /// <returns></returns> [CheckLogin] public ActionResult LogOff() { try { OperateContext.Current.RemoveCurrent(); return RedirectToAction("Login", "Account"); } catch (Exception) { return RedirectToAction("Login", "Account"); } } #endregion
那麼,上面的CheckLogin怎麼寫呢?請看以下實現(OperateContext.Current.UserInfo是我實現的上下文中獲取當前用戶的方法,Views層也可使用)。
namespace DotNet.MVC.Attributes { using DotNet.MVC.Infrastructure; /// <summary> /// CheckLoginAttribute /// 用於檢測用戶是否處於登陸狀態的標籤 /// 某些功能只須要用戶登陸就可使用 /// /// 修改紀錄 /// /// 2015-10-11 版本:1.0 SongBiao 建立文件。 /// /// <author> /// <name>SongBiao</name> /// <date>2015-10-11</date> /// </author> /// </summary> public class CheckLoginAttribute :System.Web.Mvc.AuthorizeAttribute //AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { bool pass = false; // httpContext.Session[DotNet.Business.Utilities.SessionName] //!httpContext.Request.IsAuthenticated if (OperateContext.Current.UserInfo==null) { httpContext.Response.StatusCode = 401;//無權限狀態碼 pass = false; } else { pass = true; } return pass; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { base.HandleUnauthorizedRequest(filterContext); if (filterContext.HttpContext.Response.StatusCode == 401) { filterContext.Result = new RedirectResult("/Account/Login"); } } } }
三、判斷是否具備某個Action的權限
/// <summary> /// 獲取列表數據 /// </summary> /// <param name="quoteId"></param> /// <param name="pager"></param> /// <param name="sort"></param> /// <param name="direction"></param> /// <param name="firstHaveData"></param> /// <returns></returns> [AjaxRequest] [CustomerResource("XpressionList")] public ActionResult GetList(string quoteId, QuiGridPager pager, string sort, string direction, bool firstHaveData = true) { XpressionManager xpressionManager = new XpressionManager(BusinessDbHelper, UserInfo); List<XpressionEntity> list = xpressionManager.ExecuteReaderByPage(quoteId, pager, sort, direction, "*", firstHaveData).ToList<ZtoQuoteExpressionEntity>(); return JsonQuiGridPager(pager, list, pager.totalRows, sort, direction, StartTime, firstHaveData); }
經過在Action 上添加標籤[CustomerResource("XpressionList")]實現,在過濾去中會自動去判斷當前帳號是否具備這個Action的訪問權限,這個標籤的實現以下,
namespace DotNet.MVC.Attributes { /// <summary> /// CustomerResourceAttribute /// /// 自定義的對方法應用的屬性,在Action上標註權限菜單對應的Code /// /// 修改紀錄 /// /// 2015-10-11 版本:1.0 SongBiao 建立文件。 /// /// <author> /// <name>SongBiao</name> /// <date>2015-10-11</date> /// </author> /// </summary> [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public sealed class CustomerResourceAttribute : Attribute { private readonly string _resourceName; public CustomerResourceAttribute(string resourceName) { _resourceName = resourceName; } /// <summary> /// 資源名稱 /// </summary> public string ResourceName { get { return _resourceName; } } /// <summary> /// 資源描述 /// </summary> public string Descript { get; set; } } }
這裏CustomerResourceAttribute是實現權限細分判斷的關鍵點,在權限配置中,咱們把每個Action都存儲到數據庫中,實現界面(B/S)
經過Code實現當前用戶是否具備該權限(固然也能夠經過ID),Code實現了不可重複的檢查。
目前這個過濾器底層使用了通用權限管理系統,主要用於權限菜單的存儲及判斷,數據庫及功能設計上徹底考慮到了各類形式的判斷(按用戶、公司、角色)。結合Asp.net MVC,輕輕鬆鬆就實現了權限過濾器功能。開發人員只須要會配置系統菜單及系統角色,經過接口或底層dll徹底就能夠了,開發人員只需關注業務功能的實現。
其它語言,若有相似權限判斷全局訪問過濾器的功能,也可參照實現。
對以上功能,若有疑問,歡迎一塊兒探討~~~
根據園友「wzmzang」的需求,提供OperateContext源碼,你們參考~~
namespace DotNet.MVC.Infrastructure { using DotNet.MVC.Infrastructure.Caching; using DotNet.Utilities; using DotNet.Model; using DotNet.Business; using DotNet.MVC.Models; /// <summary> /// OperateContext /// /// 上下文 /// /// 修改紀錄 /// /// 2015-09-13版本:1.0 SongBiao 建立文件。 /// /// <author> /// <name>SongBiao</name> /// <date>2015-4-13</date> /// </author> /// </summary> public class OperateContext { /// <summary> /// 祕鑰 /// </summary> private string LoginUserKey = "LoginUserKey"; /// <summary> /// 登錄提供者模式:Session、Cookie /// </summary> private string LoginProvider = BusinessSystemInfo.LoginProvider; #region public static OperateContext Current 獲取當前 操做上下文 /// <summary> /// 獲取當前 操做上下文 (爲每一個處理瀏覽器請求的服務器線程 單首創建 操做上下文) /// </summary> public static OperateContext Current { get { OperateContext oContext = CallContext.GetData(typeof(OperateContext).Name) as OperateContext; if (oContext == null) { oContext = new OperateContext(); CallContext.SetData(typeof(OperateContext).Name, oContext); } return oContext; } } #endregion #region 增長的子系統之間跳轉認證的code /// <summary> /// 生成Code /// </summary> /// <param name="openid">openid</param> /// <param name="key">向誰跳傳誰規定的key</param> /// <returns></returns> public static string MakeCode(string openid, string key) { DateTime time = DateTime.Now; string data = time.ToString("dd") + "_" + openid + "_" + time.ToString("yyyy") + "_" + key + "_" + time.ToString("MM"); MD5 md5 = new MD5CryptoServiceProvider(); byte[] result = Encoding.Default.GetBytes(data); byte[] output = md5.ComputeHash(result); data = BitConverter.ToString(output).Replace("-", "").ToLower(); SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider(); byte[] bytes = sha1.ComputeHash(Encoding.ASCII.GetBytes(data));//"596d42faf5710b35c7ea0f0a9600ee31" F69D39E1CA07FC23C1CE62F549E9D5B9780 //轉16進制 清除前面0 StringBuilder strB = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { string strX2 = bytes[i].ToString("X2"); if (strX2.Substring(0, 1) == "0") { strX2 = strX2.Substring(1, 1); } strB.Append(strX2); } return strB.ToString(); } /// <summary> /// Code驗證 /// </summary> /// <param name="openid">openid</param> /// <param name="code">待驗證的數據</param> /// <param name="key">本身系統規定的key</param> /// <returns></returns> public static bool ValidateCode(string openid, string code, string key) { string signedData = MakeCode(openid, key); return code.Equals(signedData, StringComparison.OrdinalIgnoreCase); } #endregion public BaseUserInfo _userInfo = null; /// <summary> /// 獲取當前用戶 /// </summary> /// <returns></returns> public BaseUserInfo UserInfo { get { try { if (string.Equals(LoginProvider, "Cookie", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrWhiteSpace(CookieHelper.GetCookie(LoginUserKey))) { _userInfo = JsonConvert.DeserializeObject<BaseUserInfo>(SecretUtil.Decrypt(CookieHelper.GetCookie(LoginUserKey))); } } else { if (SessionHelper.Get(LoginUserKey) != null) { _userInfo = JsonConvert.DeserializeObject<BaseUserInfo>(SecretUtil.Decrypt(SessionHelper.Get(LoginUserKey).ToString())); } } return _userInfo; } catch { throw new Exception("登陸信息超時,請從新登陸。"); } } set { _userInfo = value; } } #region Http上下文 及 相關屬性 /// <summary> /// Http上下文 /// </summary> HttpContext ContextHttp { get { return HttpContext.Current; } } /// <summary> /// 輸出對象 /// </summary> HttpResponse Response { get { return ContextHttp.Response; } } /// <summary> /// 請求對象 /// </summary> HttpRequest Request { get { return ContextHttp.Request; } } /// <summary> /// Session對象 /// </summary> System.Web.SessionState.HttpSessionState Session { get { return ContextHttp.Session; } } #endregion #region public void AddCurrent(BaseUserInfo userInfo) 寫入登陸信息 /// <summary> /// 寫入登陸信息 /// </summary> /// <param name="userInfo">成員信息</param> public void AddCurrent(BaseUserInfo userInfo) { try { userInfo.SystemCode = BaseSystemInfo.SystemCode; if (LoginProvider == "Cookie") { CookieHelper.WriteCookie(LoginUserKey, SecretUtil.Encrypt(JsonConvert.SerializeObject(userInfo)), 1440); } else { SessionHelper.Add(LoginUserKey, SecretUtil.Encrypt(JsonConvert.SerializeObject(userInfo))); } //清除上一次的 ICacheManager cacheManager = new MemoryCacheManager(); cacheManager.Clear(); } catch (Exception ex) { throw new Exception(ex.Message); } } #endregion #region public void RemoveCurrent() 刪除登陸信息 /// <summary> /// 刪除登陸信息 /// </summary> public void RemoveCurrent() { if (LoginProvider == "Cookie") { HttpCookie objCookie = new HttpCookie(LoginUserKey.Trim()); objCookie.Expires = DateTime.Now.AddYears(-5); ContextHttp.Response.Cookies.Add(objCookie); } else { SessionHelper.Remove(LoginUserKey.Trim()); } ICacheManager cacheManager = new MemoryCacheManager(); cacheManager.Clear(); } #endregion #region public bool IsOverdue() 是否過時 /// <summary> /// 是否過時 /// </summary> /// <returns></returns> public bool IsOverdue() { object str = ""; if (LoginProvider == "Cookie") { str = CookieHelper.GetCookie(LoginUserKey); } else { str = SessionHelper.Get(LoginUserKey); } if (str != null && str.ToString() != "") { return true; } else { return false; } } #endregion #region public bool IsStaticResource() 是不是訪問靜態資源 /// <summary> /// public bool IsStaticResource() 是不是訪問靜態資源 /// </summary> /// <returns></returns> public bool IsStaticResource() { if (ContextHttp == null) throw new ArgumentNullException("request"); string path = ContextHttp.Request.Path; string extension = VirtualPathUtility.GetExtension(path); if (extension == null) return false; switch (extension.ToLower()) { case ".axd": case ".ashx": case ".bmp": case ".css": case ".gif": case ".htm": case ".html": case ".ico": case ".jpeg": case ".jpg": case ".js": case ".png": case ".rar": case ".zip": return true; } return false; } #endregion #region public string ServerVariables(string name) 獲取服務器端變量 /// <summary> /// Gets server variable by name /// </summary> /// <param name="name">Name</param> /// <returns>Server variable</returns> public string ServerVariables(string name) { string result = string.Empty; try { if (ContextHttp == null || ContextHttp.Request == null) return result; //put this method is try-catch //as described here http://www.nopcommerce.com/boards/t/21356/multi-store-roadmap-lets-discuss-update-done.aspx?p=6#90196 if (ContextHttp.Request.ServerVariables[name] != null) { result = ContextHttp.Request.ServerVariables[name]; } } catch { result = string.Empty; } return result; } #endregion #region public static string GetClientIP() 獲取客戶端的IP地址 /// <summary> /// 獲取客戶端的IP地址 /// </summary> /// <returns></returns> public static string GetClientIP() { string result = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; if (!string.IsNullOrEmpty(result)) { //可能有代理 if (result.IndexOf(".", StringComparison.Ordinal) == -1) //沒有"."確定是非IPv4格式 result = null; else { if (result.IndexOf(",", StringComparison.Ordinal) != -1) { //有",",估計多個代理。取第一個不是內網的IP。 result = result.Replace(" ", "").Replace("", ""); string[] temparyip = result.Split(",;".ToCharArray()); for (int i = 0; i < temparyip.Length; i++) { if (IsIPAddress(temparyip[i]) && temparyip[i].Substring(0, 3) != "10." && temparyip[i].Substring(0, 7) != "192.168" && temparyip[i].Substring(0, 7) != "172.16.") { return temparyip[i];//找到不是內網的地址 } } } else if (IsIPAddress(result)) //代理便是IP格式 return result; else result = null;//代理中的內容 非IP,取IP } } string ipAddress = (HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null && HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != String.Empty) ? HttpContext.Current.Request.ServerVariables ["HTTP_X_FORWARDED_FOR"] : HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"]; if (string.IsNullOrEmpty(result)) result = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"]; if (string.IsNullOrEmpty(result)) result = HttpContext.Current.Request.UserHostAddress; return result; } /// <summary> /// ip判斷 /// </summary> /// <param name="str1"></param> /// <returns></returns> private static bool IsIPAddress(string str1) { if (string.IsNullOrEmpty(str1) || str1.Length < 7 || str1.Length > 15) return false; string regformat = @"^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$"; Regex regex = new Regex(regformat, RegexOptions.IgnoreCase); return regex.IsMatch(str1); } #endregion /// <summary> /// 解決Error"基礎鏈接已經關閉: 未能爲SSL/TLS 安全通道創建信任關係。" /// </summary> /// <param name="sender"></param> /// <param name="certificate"></param> /// <param name="chain"></param> /// <param name="sslPolicyErrors"></param> /// <returns></returns> private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; } /// <summary> /// 業務庫鏈接 /// </summary> public static IDbHelper BusinessDbHelper { get { return DbHelperFactory.GetHelper(BaseSystemInfo.BusinessDbType, BaseSystemInfo.BusinessDbConnection); } } /// <summary> /// 用戶中心庫鏈接 /// </summary> public static IDbHelper UserCenterDbHelper { get { return DbHelperFactory.GetHelper(BaseSystemInfo.UserCenterDbType, BaseSystemInfo.UserCenterDbConnection); } } } }