[toc] 前言 在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善, 倉庫地址: "April.Util_github" , "April.Util_ ...
目錄
前言
在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善,倉庫地址: April.Util_github,April.Util_gitee,還沒關註的朋友希望可以先mark,後續會持續維護。
許可權
在之前的net core WebApi——公用庫April.Util公開及發佈中已經介紹了初次發佈的一些功能,其中包括緩存,日誌,加密,統一的配置等等,具體可以再回頭看下這篇介紹,而在其中有個TokenUtil,因為當時發佈的時候這塊兒還沒有更新上,趁著周末來整理下吧。
關於webapi的許可權,可以藉助Identity,Jwt,但是我這裡沒有藉助這些,只是自己做了個token的生成已經存儲用戶主要信息,對於許可權我想大多數人已經有了一套自己的許可權體系,所以這裡我簡單介紹下我的思路。
- 首先對於菜單做許可權標示,請求的控制器,請求的事件
- 菜單信息維護後,設置角色對應多個菜單
- 管理員對應多個角色
- 在登錄的時候根據賬號信息獲取對應管理員的角色及最終菜單,控制器,事件
- 處理管理員信息後自定義token,可設置token過期時間,token可以反解析(如果到期自動重新授權,我這裡沒有處理)
- 每次訪問介面的時候(除公開不需校驗的介面),根據請求的路徑判斷是否有當前控制器許可權(通過中間層),進入介面後判斷是否有對應許可權(通過標簽)
通過上述流程來做許可權的校驗,當然這裡只是針對單應用,如果是多應用的話,這裡還要考慮應用問題(如,一個授權認證工程主做身份校驗,多個應用工程通用一個管理)。
首先,我們需要一個可以存儲管理員的對應屬性集合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版本的東西,也可以看代碼倉庫中的示例工程。
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來完成登錄授權這塊兒,至於數據的劃分,畢竟這個東西不是通用的,所以只是點出來而沒有去整合,如果有好的建議或者自己整合的通用類庫也可以跟我交流。