在asp.net core2.1中新增中介軟體以擴充套件Swashbuckle.AspNetCore3.0支援簡單的文件訪問許可權控制
Swashbuckle.AspNetCore3.0 介紹
一個使用 ASP.NET Core 構建的 API 的 Swagger 工具。直接從您的路由,控制器和模型生成漂亮的 API 文件,包括用於探索和測試操作的 UI。
專案主頁: ofollow,noindex" target="_blank">https://github.com/domaindrivendev/Swashbuckle.AspNetCore
劃重點,使用多看看 Readme,然後看下 專案官方示例 ,遇到問題找找 issues
繼上篇 Swashbuckle.AspNetCore3.0 的二次封裝與使用 分享了二次封裝的程式碼,本篇將分享如何給文件新增一個登入頁,控制文件的訪問許可權(文末附完整 Demo)
關於生產環境介面文件的顯示
在此之前的介面專案中,若使用了 Swashbuckle.AspNetCore,都是控制其只在開發環境使用,不會就這樣將其釋出到生產環境(安全第一) 。
那麼,怎麼安全的釋出 swagger 呢?我有兩種想法
- 將路由字首改得超級複雜
- 新增一個攔截器控制 swagger 文件的訪問必須獲得授權(登入)
大佬若有更好的想法,還望指點一二
下面我將介紹基於 asp.net core2.1 且使用了 Swashbuckle.AspNetCore3.0 的專案種是怎麼去實現安全校驗的
通過本篇文章之後,可以放心的將專案中的 swagger 文件釋出到生產環境,並使其可通過使用者名稱密碼去登入訪問,得以安全且方便的測試介面。
實現思路
前面已經說到,需要一個攔截器,而這個攔截器還需要是全域性的,在 asp.net core 中,自然就需要用到的是 中介軟體 了
步驟如下,在 UseSwagger 之前使用自定義的中介軟體
攔截所有 swagger 相關請求,判斷是否授權登入
若未登入則跳轉到授權登入頁,登入後即可訪問 swagger 的資源
如果專案本身有登入系統,可在自定義中介軟體中使用專案中的登入,
沒有的話,我會分享一個簡單的使用者密碼登入的方案
Demo 如下圖所示
為使用 Swashbuckle.AspNetCore3 的專案新增介面文件登入功能
在寫此功能之前,已經封裝了一部分程式碼,此功能算是在此之前的程式碼封裝的一部分,不過是後面完成的。文中程式碼刪除了耦合,和 demo 中會有一點差異。
定義模型存放使用者密碼
public class CustomSwaggerAuth { public CustomSwaggerAuth() { } public CustomSwaggerAuth(string userName,string userPwd) { UserName = userName; UserPwd = userPwd; } public string UserName { get; set; } public string UserPwd { get; set; } //加密字串 public string AuthStr { get { return SecurityHelper.HMACSHA256(UserName + UserPwd); } } }
加密方法(HMACSHA256)
public static string HMACSHA256(string srcString, string key="abc123") { byte[] secrectKey = Encoding.UTF8.GetBytes(key); using (HMACSHA256 hmac = new HMACSHA256(secrectKey)) { hmac.Initialize(); byte[] bytes_hmac_in = Encoding.UTF8.GetBytes(srcString); byte[] bytes_hamc_out = hmac.ComputeHash(bytes_hmac_in); string str_hamc_out = BitConverter.ToString(bytes_hamc_out); str_hamc_out = str_hamc_out.Replace("-", ""); return str_hamc_out; } }
自定義中介軟體
此中介軟體中有使用的 login.html,其屬性均為內嵌資源,故事用 GetManifestResourceStream 讀取檔案流並輸出,這樣可以方便的將其進行封裝到獨立的類庫中,而不與輸出專案耦合
關於退出按鈕,可以參考前文自定義 index.html
private const string SWAGGER_ATUH_COOKIE = nameof(SWAGGER_ATUH_COOKIE); public void Configure(IApplicationBuilder app) { var options=new { RoutePrefix="swagger", SwaggerAuthList = new List<CustomSwaggerAuth>() { new CustomSwaggerAuth("swaggerloginer","123456") }, } var currentAssembly = typeof(CustomSwaggerAuth).GetTypeInfo().Assembly; app.Use(async (context, next) => { var _method = context.Request.Method.ToLower(); var _path = context.Request.Path.Value; // 非swagger相關請求直接跳過 if (_path.IndexOf($"/{options.RoutePrefix}") != 0) { await next(); return; } else if (_path == $"/{options.RoutePrefix}/login.html") { //登入 if (_method == "get") { //讀取CustomSwaggerAuth所在程式集內嵌的login.html並輸出 var stream = currentAssembly.GetManifestResourceStream($"{currentAssembly.GetName().Name}.login.html"); byte[] buffer = new byte[stream.Length]; stream.Read(buffer, 0, buffer.Length); context.Response.ContentType = "text/html;charset=utf-8"; context.Response.StatusCode = StatusCodes.Status200OK; context.Response.Body.Write(buffer, 0, buffer.Length); return; } else if (_method == "post") { var userModel = new CustomSwaggerAuth(context.Request.Form["userName"], context.Request.Form["userPwd"]); if (!options.SwaggerAuthList.Any(e => e.UserName == userModel.UserName && e.UserPwd == userModel.UserPwd)) { await context.Response.WriteAsync("login error!"); return; } //context.Response.Cookies.Append("swagger_auth_name", userModel.UserName); context.Response.Cookies.Append(SWAGGER_ATUH_COOKIE, userModel.AuthStr); context.Response.Redirect($"/{options.RoutePrefix}"); return; } } else if (_path == $"/{options.RoutePrefix}/logout") { //退出 context.Response.Cookies.Delete(SWAGGER_ATUH_COOKIE); context.Response.Redirect($"/{options.RoutePrefix}/login.html"); return; } else { //若未登入則跳轉登入 if (!options.SwaggerAuthList.Any(s => !string.IsNullOrEmpty(s.AuthStr) && s.AuthStr == context.Request.Cookies[SWAGGER_ATUH_COOKIE])) { context.Response.Redirect($"/{options.RoutePrefix}/login.html"); return; } } await next(); }); app.UseSwagger(); app.UseSwaggerUI(c=>{ if (options.SwaggerAuthList.Count > 0) { //index.html中新增ConfigObject屬性 c.ConfigObject["customAuth"] = true; c.ConfigObject["loginUrl"] = $"/{options.RoutePrefix}/login.html"; c.ConfigObject["logoutUrl"] = $"/{options.RoutePrefix}/logout"; } }); app.UseMvc(); }
index.html 新增退出按鈕
if (configObject.customAuth) { var logOutEle = document.createElement('button') logOutEle.className = 'btn ' logOutEle.innerText = '退出' logOutEle.onclick = function() { location.href = configObject.logoutUrl } document.getElementsByClassName('topbar-wrapper')[0].appendChild(logOutEle) }
自定義的 index.html,login.html
注意:需要將其改為內嵌資源(屬性->生成操作->嵌入的資源)