在以前已經提到過,公用類庫Util已經開源,目的一是爲了簡化開發的工做量,畢竟有些常規的功能類庫重複率仍是挺高的,二是爲了一塊兒探討學習軟件開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善,倉庫地址: April.Util_github,April.Util_gitee,還沒關注的朋友但願能夠先mark,後續會持續維護。git
在以前的net core WebApi——公用庫April.Util公開及發佈中已經介紹了初次發佈的一些功能,其中包括緩存,日誌,加密,統一的配置等等,具體能夠再回頭看下這篇介紹,而在其中有個TokenUtil,由於當時發佈的時候這塊兒尚未更新上,趁着週末來整理下吧。github
關於webapi的權限,能夠藉助Identity,Jwt,可是我這裏沒有藉助這些,只是本身作了個token的生成已經存儲用戶主要信息,對於權限我想大多數人已經有了一套本身的權限體系,因此這裏我簡單介紹下個人思路。web
經過上述流程來作權限的校驗,固然這裏只是針對單應用,若是是多應用的話,這裏還要考慮應用問題(如,一個受權認證工程主作身份校驗,多個應用工程通用一個管理)。api
首先,咱們須要一個能夠存儲管理員的對應屬性集合AdminEntity,主要存儲基本信息,控制器集合,權限集合,數據集合(也就是企業部門等)。緩存
/// <summary> /// 管理員實體 /// </summary> public class AdminEntity { private int _ID = -1; private string _UserName = string.Empty; private string _Avator = string.Empty; private List<string> _Controllers = new List<string>(); private List<string> _Permissions = new List<string>(); private int _TokenType = 0; private bool _IsSuperManager = false; private List<int> _Depts = new List<int>(); private int _CurrentDept = -1; private DateTime _ExpireTime = DateTime.Now; /// <summary> /// 主鍵 /// </summary> public int ID { get => _ID; set => _ID = value; } /// <summary> /// 用戶名 /// </summary> public string UserName { get => _UserName; set => _UserName = value; } /// <summary> /// 頭像 /// </summary> public string Avator { get => _Avator; set => _Avator = value; } /// <summary> /// 控制器集合 /// </summary> public List<string> Controllers { get => _Controllers; set => _Controllers = value; } /// <summary> /// 權限集合 /// </summary> public List<string> Permissions { get => _Permissions; set => _Permissions = value; } /// <summary> /// 訪問方式 /// </summary> public int TokenType { get => _TokenType; set => _TokenType = value; } /// <summary> /// 是否爲超管 /// </summary> public bool IsSuperManager { get => _IsSuperManager; set => _IsSuperManager = value; } /// <summary> /// 企業集合 /// </summary> public List<int> Depts { get => _Depts; set => _Depts = value; } /// <summary> /// 當前企業 /// </summary> public int CurrentDept { get => _CurrentDept; set => _CurrentDept = value; } /// <summary> /// 過時時間 /// </summary> public DateTime ExpireTime { get => _ExpireTime; set => _ExpireTime = value; } }
以後咱們來完成TokenUtil這塊兒,首先是生成咱們的token串,由於考慮到須要反解析,因此這裏採用的是字符串加解密,固然這個加密串具體是什麼能夠自定義,目前我這裏設置的是固定須要兩個參數{id},{ts},目的是爲了保證加密串的惟一,固然也是爲了過時無感知從新受權準備的。異步
public class TokenUtil { /// <summary> /// 設置token /// </summary> /// <returns></returns> public static string GetToken(AdminEntity user, out string expiretimstamp) { string id = user.ID.ToString(); double exp = 0; switch ((AprilEnums.TokenType)user.TokenType) { case AprilEnums.TokenType.Web: exp = AprilConfig.WebExpire; break; case AprilEnums.TokenType.App: exp = AprilConfig.AppExpire; break; case AprilEnums.TokenType.MiniProgram: exp = AprilConfig.MiniProgramExpire; break; case AprilEnums.TokenType.Other: exp = AprilConfig.OtherExpire; break; } DateTime date = DateTime.Now.AddHours(exp); user.ExpireTime = date; double timestamp = DateUtil.ConvertToUnixTimestamp(date); expiretimstamp = timestamp.ToString(); string token = AprilConfig.TokenSecretFormat.Replace("{id}", id).Replace("{ts}", expiretimstamp); token = EncryptUtil.EncryptDES(token, EncryptUtil.SecurityKey); //LogUtil.Debug($"用戶{id}獲取token:{token}"); Add(token, user); //處理多點登陸 SetUserToken(token, user.ID); return token; } /// <summary> /// 經過token獲取當前人員信息 /// </summary> /// <param name="token"></param> /// <returns></returns> public static AdminEntity GetUserByToken(string token = "") { if (string.IsNullOrEmpty(token)) { token = GetTokenByContent(); } if (!string.IsNullOrEmpty(token)) { AdminEntity admin = Get(token); if (admin != null) { //校驗時間 if (admin.ExpireTime > DateTime.Now) { if (AprilConfig.AllowSliding) { //延長時間 admin.ExpireTime = DateTime.Now.AddMinutes(30); //更新 Add(token, admin); } return admin; } else { //已通過期的就再也不延長了,固然後續根據狀況改進吧 return null; } } } return null; } /// <summary> /// 經過用戶請求信息獲取Token信息 /// </summary> /// <returns></returns> public static string GetTokenByContent() { string token = ""; //判斷header var headers = AprilConfig.HttpCurrent.Request.Headers; if (headers.ContainsKey("token")) { token = headers["token"].ToString(); } if (string.IsNullOrEmpty(token)) { token = CookieUtil.GetString("token"); } if (string.IsNullOrEmpty(token)) { AprilConfig.HttpCurrent.Request.Query.TryGetValue("token", out StringValues temptoken); if (temptoken != StringValues.Empty) { token = temptoken.ToString(); } } return token; } /// <summary> /// 移除Token /// </summary> /// <param name="token"></param> public static void RemoveToken(string token = "") { if (string.IsNullOrEmpty(token)) { token = GetTokenByContent(); } if (!string.IsNullOrEmpty(token)) { Remove(token); } } #region 多個登陸 /// <summary> /// 多個登陸設置緩存 /// </summary> /// <param name="token"></param> /// <param name="userid"></param> public static void SetUserToken(string token, int userid) { Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken"); if (dicusers == null) { dicusers = new Dictionary<int, List<string>>(); } List<string> listtokens = new List<string>(); if (dicusers.ContainsKey(userid)) { listtokens = dicusers[userid]; if (listtokens.Count <= 0) { listtokens.Add(token); } else { if (!AprilConfig.AllowMuiltiLogin) { foreach (var item in listtokens) { RemoveToken(item); } listtokens.Add(token); } else { bool isAdd = true; foreach (var item in listtokens) { if (item == token) { isAdd = false; } } if (isAdd) { listtokens.Add(token); } } } } else { listtokens.Add(token); dicusers.Add(userid, listtokens); } CacheUtil.Add("UserToken", dicusers, new TimeSpan(6, 0, 0), true); } /// <summary> /// 多個登陸刪除緩存 /// </summary> /// <param name="userid"></param> public static void RemoveUserToken(int userid) { Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken"); if (dicusers != null && dicusers.Count > 0) { if (dicusers.ContainsKey(userid)) { //刪除全部token var listtokens = dicusers[userid]; foreach (var token in listtokens) { RemoveToken(token); } dicusers.Remove(userid); } } } /// <summary> /// 多個登陸獲取 /// </summary> /// <param name="userid"></param> /// <returns></returns> public static List<string> GetUserToken(int userid) { Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken"); List<string> lists = new List<string>(); if (dicusers != null && dicusers.Count > 0) { foreach (var item in dicusers) { if (item.Key == userid) { lists = dicusers[userid]; break; } } } return lists; } #endregion #region 私有方法(這塊兒還須要改進) private static void Add(string token,AdminEntity admin) { switch (AprilConfig.TokenCacheType) { //不推薦Cookie case AprilEnums.TokenCacheType.Cookie: CookieUtil.Add(token, admin); break; case AprilEnums.TokenCacheType.Cache: CacheUtil.Add(token, admin, new TimeSpan(0, 30, 0)); break; case AprilEnums.TokenCacheType.Session: SessionUtil.Add(token, admin); break; case AprilEnums.TokenCacheType.Redis: RedisUtil.Add(token, admin); break; } } private static AdminEntity Get(string token) { AdminEntity admin = null; switch (AprilConfig.TokenCacheType) { case AprilEnums.TokenCacheType.Cookie: admin = CookieUtil.Get<AdminEntity>(token); break; case AprilEnums.TokenCacheType.Cache: admin = CacheUtil.Get<AdminEntity>(token); break; case AprilEnums.TokenCacheType.Session: admin = SessionUtil.Get<AdminEntity>(token); break; case AprilEnums.TokenCacheType.Redis: admin = RedisUtil.Get<AdminEntity>(token); break; } return admin; } private static void Remove(string token) { switch (AprilConfig.TokenCacheType) { case AprilEnums.TokenCacheType.Cookie: CookieUtil.Remove(token); break; case AprilEnums.TokenCacheType.Cache: CacheUtil.Remove(token); break; case AprilEnums.TokenCacheType.Session: SessionUtil.Remove(token); break; case AprilEnums.TokenCacheType.Redis: RedisUtil.Remove(token); break; } } #endregion }
固然這也在以前已經提到過net core Webapi基礎工程搭建(七)——小試AOP及常規測試_Part 1,當時還以爲這個叫作攔截器,too young too simple,至於使用方法這裏就很少說了,能夠參考以前2.2版本的東西,也能夠看代碼倉庫中的示例工程。async
public class AprilAuthorizationMiddleware { private readonly RequestDelegate next; public AprilAuthorizationMiddleware(RequestDelegate next) { this.next = next; } public Task Invoke(HttpContext context) { if (context.Request.Method != "OPTIONS") { string path = context.Request.Path.Value; if (!AprilConfig.AllowUrl.Contains(path)) { //獲取管理員信息 AdminEntity admin = TokenUtil.GetUserByToken(); if (admin == null) { //從新登陸 return ResponseUtil.HandleResponse(-2, "未登陸"); } if (!admin.IsSuperManager) { //格式統一爲/api/Controller/Action,兼容多級如/api/Controller1/ConrolerInnerName/xxx/Action string[] strValues = System.Text.RegularExpressions.Regex.Split(path, "/"); string controller = ""; bool isStartApi = false; if (path.StartsWith("/api")) { isStartApi = true; } for (int i = 0; i < strValues.Length; i++) { //爲空,爲api,或者最後一個 if (string.IsNullOrEmpty(strValues[i]) || i == strValues.Length - 1) { continue; } if (isStartApi && strValues[i] == "api") { continue; } if (!string.IsNullOrEmpty(controller)) { controller += "/"; } controller += strValues[i]; } if (string.IsNullOrEmpty(controller)) { controller = strValues[strValues.Length - 1]; } if (!admin.Controllers.Contains(controller.ToLower())) { //無權訪問 return ResponseUtil.HandleResponse(401, "無權訪問"); } } } } return next.Invoke(context); } }
Ok,咱們先來看下Login中的操做以及實現效果吧。函數
[HttpPost] public async Task<ResponseDataEntity> Login(LoginFormEntity formEntity) { if (string.IsNullOrEmpty(formEntity.LoginName) || string.IsNullOrEmpty(formEntity.Password)) { return ResponseUtil.Fail("請輸入帳號密碼"); } if (formEntity.LoginName == "admin") { //這裏實際應該經過db獲取管理員 string password = EncryptUtil.MD5Encrypt(formEntity.Password, AprilConfig.SecurityKey); if (password == "B092956160CB0018") { //獲取管理員相關權限,一樣是db獲取,這裏只作展現 AdminEntity admin = new AdminEntity { UserName = "超級管理員", Avator = "", IsSuperManager = true, TokenType = (int)AprilEnums.TokenType.Web }; string token = TokenUtil.GetToken(admin, out string expiretimestamp); int expiretime = 0; int.TryParse(expiretimestamp, out expiretime); //能夠考慮記錄登陸日誌等其餘信息 return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime }); } } else if (formEntity.LoginName == "test") { //這裏作權限演示 AdminEntity admin = new AdminEntity { UserName = "測試", Avator = "", TokenType = (int)AprilEnums.TokenType.Web }; admin.Controllers.Add("weatherforecast"); admin.Permissions.Add("weatherforecast_log");//控制器_事件(Add,Update...) string token = TokenUtil.GetToken(admin, out string expiretimestamp); int expiretime = 0; int.TryParse(expiretimestamp, out expiretime); //能夠考慮記錄登陸日誌等其餘信息 return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime }); } //這裏其實已經能夠考慮驗證碼相關了,可是這是示例工程,後續可持續關注我,會有基礎工程(帶權限)的實例公開 return ResponseUtil.Fail("帳號密碼錯誤"); }
可能乍一看會先吐槽下,明明是異步接口還用同步的方法,沒有異步的實現空浪費內存xxx,由於db考慮是要搞異步,因此這裏示例就這樣先寫了,主要是領會精神,咳咳。學習
來試下效果吧,首先咱們隨便訪問個白名單外的接口。
而後咱們經過帳號登錄Login接口(直接寫死了,admin,123456),獲取到token。
而後咱們來訪問接口。
是否是仍是未登陸,沒錯,由於沒有token的傳值,固然我這裏是經過query傳值,支持header,token,query。
這裏由於是超管,因此權限隨意搞,無所謂,接下來展現下普通用戶的權限標示。
目前能夠經過標籤AprilPermission,把當前的控制器與對應事件的權限做爲參數傳遞,以後根據當前管理員信息作校驗。
public class AprilPermissionAttribute : Attribute, IActionFilter { public string Permission; public string Controller; /// <summary> /// 構造函數 /// </summary> /// <param name="_controller">控制器</param> /// <param name="_permission">接口事件</param> public AprilPermissionAttribute(string _controller, string _permission) { Permission = _permission; Controller = _controller; } public void OnActionExecuted(ActionExecutedContext context) { LogUtil.Debug("AprilPermission OnActionExecuted"); } public void OnActionExecuting(ActionExecutingContext context) { AdminEntity admin = TokenUtil.GetUserByToken(); if (admin == null || admin.ExpireTime <= DateTime.Now) { context.Result = new ObjectResult(new { msg = "未登陸", code = -2 }); } if (!admin.IsSuperManager) { string controller_permission = $"{Controller}_{Permission}"; if (!admin.Controllers.Contains(Controller) || !admin.Permissions.Contains(controller_permission)) { context.Result = new ObjectResult(new { msg = "無權訪問", code = 401 }); } } } }
針對幾個接口作了調整,附上標籤後判斷權限,咱們來測試下登陸test,密碼隨意。
至此權限相關的功能也統一塊兒來,固然若是有個性化的仍是須要調整的,後續也是會不斷的更新改動。
權限仍是稍微麻煩點兒啊,經過中間層,標籤以及TokenUtil來完成登陸受權這塊兒,至於數據的劃分,畢竟這個東西不是通用的,因此只是點出來而沒有去整合,若是有好的建議或者本身整合的通用類庫也能夠跟我交流。