在普通的MVC項目中 我們普遍的使用Cookie來作為認證授權方式,使用簡單。登錄成功后將用戶信息寫入Cookie;但當我們做WebApi的時候顯然Cookie這種方式就有點不適用了。
在.NET Core 中 WebApi中目前比較流行的認證授權方式是Jwt (Json Web Token) 技術。Jwt 是一種無狀態的分布式身份驗證方式,Jwt 是將用戶登錄信息加密后存放到返回的Token中 ,相當於用戶信息是存儲在客戶端。Jwt的加密方式有兩種 :對稱加密與非對稱加密,非對稱加密即 RSA 加密的方式。
自己手寫認證授權代碼和Jwt的思路是一樣的;不同之處在於:
1、加密方式僅僅是采用的對稱加密方式 簡單高效。哈哈!(弊端就是沒有非對稱加密更安全);
2、用戶登錄信息主要保存在Redis中,即服務端。
自己寫的好處:
1、擴展性強,可根據自己的需要進行各種擴展,比如在驗證授權時可很方便的添加多設備登錄擠下線功能等。
2、可隨時調整用戶的Token失效時間。
認證及授權流程
1、先請求登錄接口,登錄成功,為用戶產生一個Token,
登錄獲取Token 圖片中ticket字段。
2、 客戶端拿到Token在其他請求中將Token信息添加到請求頭中傳遞到服務端。
開發思路
1、添加一個過濾器。在Startup 中ConfigureServices方法里添加一個Filters 即我們自己授權代碼類。
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddMvc(mvc => { //添加自己的授權驗證 mvc.Filters.Add(typeof(AuthorizeFilter)); }); }
具體詳細代碼,請看文章結尾github地址。
添加過濾器之后我們的每次請求都會優先執行過濾器的代碼。在這里我們就可以判斷用戶是否已經登錄,從而進行攔截沒有授權的的請求。
/// <summary> /// 安全認證過濾器 /// </summary> public class AuthorizeFilter : IActionFilter, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { //允許匿名訪問 if (context.HttpContext.User.Identity.IsAuthenticated || context.Filters.Any(item => item is IAllowAnonymousFilter)) return; var httpContext = context.HttpContext; var claimsIdentity = httpContext.User.Identity as ClaimsIdentity; var request = context.HttpContext.Request; var authorization = request.Headers["Authorization"].ToString(); if (authorization != null && authorization.Contains("BasicAuth")) { //獲取請求頭中傳遞的ticket var current_ticket = authorization.Split(" ")[1]; //校驗ticket並獲取用戶信息 var userInfo = TicketEncryption.VerifyTicket(current_ticket, out string dec_client); if (userInfo != null) { //同一個終端多次登錄擠下線功能 返回403 if (userInfo.ticket != current_ticket && userInfo.client.ToString() == dec_client) { #region 多設備擠下線代碼 var response = new HttpResponseMessage(); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Result = new JsonResult("Forbidden:The current authorization has expired"); #endregion return; } else { return; } } } // 401 未授權 context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; context.Result = new JsonResult("Forbidden:Tiket Invalid"); } }
具體詳細代碼,請看文章結尾github地址。
2、登錄並獲取Token
由於添加了IAuthorizationFilter類型的過濾器,所以每個請求都會被攔截。所以登錄接口我們需要允許匿名訪問。
3、加解密Token
加密:登錄成功后就要產生個Token了,產生也簡單。將用戶的唯一信息比如uid或者guid進行對稱式加密。當然如果需要對登錄設備做區分或者多設備登錄擠下線功能時最好也將登錄設備一起加密進去。
我們都知道 在加密中一般情況下只要加密的數據及加密key不變;那么加密后的內容也會一直保持不變。如果我們每次登錄產生的Token一直沒有任何變化只要這個Token被泄露了那將很危險的。竟然我們希望每次登錄產生的Token都有變化。那就要改變加密數據或者加密key了。加密數據是用戶唯一信息這個顯然不可能產生變化。所以我們能改變的地方只能是加密key了;我們采用固定key+隨機key的方式。
因為加密key在我們解密時也是需要一一對應的。所以我們得想辦法將我們的隨機key告訴我們解密的代碼中。辦法就是 我們將加密后的內容(一般情況進行base64編碼)再加上隨機key。(隨機key一定是固定長度 不然后面無法解析拆分)
比如加密內容是guid=73e01eab-210d-4d19-a72a-d0d64e053ec0+client=ios 固定key=123654+隨機key=FEZaaWbyimaWiJHah
即加密過程:
加密(73e01eab-210d-4d19-a72a-d0d64e053ec0&ios,123654FEZaaWbyimaWiJHah)=M0EzM0ZGRjk2QzgwRDY2RDJDMTdFOEJGRUE0NDI3NEE1RDlFNkU4NDQ0MERFNEIyMkQ5QjM4MjAxODcwj加隨機key:FEZaaWbyimaWiJHah
所以我們返回給用戶的Token實際上是包含了隨機key的。當然這個隨機key只有我們自己知道。因為隨機key的長度以及位置只有我們自己知道。這種方式即使我們固定key被泄露了 只要別人不知道我們隨機key處理方法也無濟於事。
解密:知道加密過程后就好解密了。拿到用戶提交的Token后首先按照隨機key的固定位置進行截取。將加密內容和隨機key拆開。然后將固定key和隨機key組合一起解密加密的內容,取得用戶guid和登錄的客戶端類型。
完整加解密代碼
代碼中的ticket代表本文中的Token。代碼中使用的是DES加解密
public class TicketEncryption { //加密key 實際中請用配置文件配置 private static readonly string key = "yvDlky7GXGtlPCGr"; /// <summary> /// 獲取一個新的ticket /// </summary> /// <param name="guid">用戶的guid</param> /// <param name="client">客戶端</param> /// <returns></returns> public static string GenerateTicket(string guid, string client) { //隨機key string randomKey = Randoms.GetRandomString(15); var keys = key + randomKey; var desStr = Encryption.DesEncrypt(guid + "&" + client, keys); var base64Str = Encryption.Base64Encrypt(desStr) + randomKey; return base64Str; } /// <summary> /// 校驗ticket /// </summary> /// <param name="encryptStr"></param> /// <returns></returns> public static UserInfo VerifyTicket(string encryptStr,out string client) { try { RedisHelper redisHelper = new RedisHelper("127.0.0.1:6379"); //加密原型:guid&client; 如:08e80f78-95ad-427c-b506-a5f1504e29ac&ios string randomKey = encryptStr.Substring(encryptStr.Length - 15); var base64 = encryptStr.Substring(0, encryptStr.Length - 15); var deBase64 = Encryption.Base64Decrypt(base64); var keys = key + randomKey; string ticketInfo = Encryption.DesDecrypt(deBase64, keys); var guid = ticketInfo.Split("&")[0]; client = ticketInfo.Split("&")[1]; string redisKey = "ticket_" + guid; var obj = redisHelper.Get<UserInfo>(redisKey); return obj; } catch (Exception ex) { throw ex; } } }
完整demo代碼請看github
https://github.com/cfan1236/NetCoreAuthorize