前言
哈嘍大家好,馬上就要年末了,距離新的一年,只有50天了,春節是75天。
在這個時節內,天氣逐漸變涼,但是大家的心肯定很熱吧,因為發生了兩件大事:
1、雙十一買買買,在這個讓人激動又糾結的一天,大家有沒有被像 “高考命題組” 般的優惠方案搞得雲里來霧里去?最終,我選擇了 <不買東西優惠100%> 的最優解方案,其實是 Q I O N G 第二聲😂。
2、還有一個特別轟動的,當屬前兩天上海舉辦的 "中國.NET開發者峰會,可以這樣讀NET Conf(c,o,n ,f)",真的特別轟動,各路大神齊聚,只可惜我當時正在開啟微講堂,為了履行我上次公眾號點贊的諾言——10小時入門net core的遠程視頻授課🤣。
言歸正傳,曾幾何時,在某微信群討論 Http 狀態碼的時候,被某大佬給懟了一下,具體的內容就不說了,反正現在的返回狀態碼無非就那兩個方案,一個是用 RESTFul 風格,完全通過 http狀態碼來處理,另一個就是通過 自定義返回內容,比如json的格式,把狀態信息放到返回內容里邊,最終我沒有聽從他的意見,還是堅持我自己的風格(狀態碼+自定義格式),具體的內容我都會在下面詳細的說明的,恰逢QQ群里有一個小伙伴也說到了關於封裝狀態碼的問題,其實我已經寫了,只不過他的更優雅,更漂亮,所以我就用他的方案了:
投稿人:QQ群:菜工 、 飛非→飛
主題:封裝授權認證的自定義返回格式。
代碼:Blog.Core 主分支
具體內容:詳見下文。
一、兩種返回格式的思考
在上邊的文章中呢,我和某大佬基於返回格式簡單的表明了下個人的立場,其實我自己也懂,無非就那么兩個情況:
1、完全基於 HTTP 返回格式狀態碼
說這個可能有點兒抽象,我舉個例子大家就懂了:
namespace Microsoft.AspNetCore.Http { public static class StatusCodes { public const int Status100Continue = 100; public const int Status101SwitchingProtocols = 101; public const int Status102Processing = 102; public const int Status200OK = 200; // 等等等等 public const int Status400BadRequest = 400; public const int Status401Unauthorized = 401; public const int Status402PaymentRequired = 402; public const int Status403Forbidden = 403; public const int Status404NotFound = 404; public const int Status405MethodNotAllowed = 405; public const int Status406NotAcceptable = 406; public const int Status414RequestUriTooLong = 414; public const int Status414UriTooLong = 414; public const int Status415UnsupportedMediaType = 415; public const int Status416RangeNotSatisfiable = 416; public const int Status416RequestedRangeNotSatisfiable = 416; public const int Status417ExpectationFailed = 417; public const int Status418ImATeapot = 418; public const int Status419AuthenticationTimeout = 419; public const int Status421MisdirectedRequest = 421; public const int Status422UnprocessableEntity = 422; public const int Status423Locked = 423; public const int Status424FailedDependency = 424; // 等等等等 public const int Status500InternalServerError = 500; public const int Status501NotImplemented = 501; public const int Status502BadGateway = 502; public const int Status503ServiceUnavailable = 503; public const int Status504GatewayTimeout = 504; public const int Status505HttpVersionNotsupported = 505; public const int Status506VariantAlsoNegotiates = 506; public const int Status507InsufficientStorage = 507; public const int Status508LoopDetected = 508; public const int Status510NotExtended = 510; public const int Status511NetworkAuthenticationRequired = 511; } }
上邊的就是官方給定的 Http 狀態碼,我刪了一些,大家可以看出來,官方給的特別多,也特別的全,已經能滿足我們平時開發的所有需要,完全沒問題,而且呢,這樣還有一個好處,就是比如前端的項目,比如 VUE ,可以根據 http 狀態碼來進行攔截器進行封裝,而不用看返回結果了,單單從 statuscode 上,就直接統一攔截,這樣看似特別完美,那為啥還會有第二種解決方案呢,請繼續往下看。
2、自定義返回格式內容
上邊的方法真的就特別完美么,首先,攔截器這個優點,並不是只能用在攔截 http statuscode 上,針對具體的返回內容也可以攔截。
其次,大家可能偶爾會遇到過這個情況,就是訪問微信或者什么的時候,會出現提示 “5003 xxxxxx異常”,大家可以看一下,這個返回狀態碼,http 是沒有的。
而且,websocket 也並沒有那些所謂的 404 、503吧,這個時候就需要我們去自定義,比如這樣的:
這就是第二種解決方案,這兩種方案其實一直都存在我們的平時開發過程中的,當然我是都在用的,我目前自己的開源項目里,用的是第一種解決方案,偶爾也會有第二種,公司的某些項目里,用的是第二種,因為有時候狀態信息太多,必須去自定義,所以這兩種方案我都是支持的,也不用說這個不對,那個錯誤,而且我也同時用了這兩個。
那既然兩種都支持,如果兩個我都想用,怎么封裝一下呢,沒問題,我就在 Blog.Core 項目里,對 授權認證 返回格式封裝一下,大家看看吧,原理我以后會在直播里講,這里就不細說了,直接講操作步驟。
二、自定義授權認證返回格式
1、復雜的策略授權
那既然說到了返回格式,肯定得有一個場景,那我就用我的復雜策略授權 PermissionHandler.cs 來舉例子,大家平時也都用過,我在本周三的直播中,會詳細說名這個復雜策略授權的運行機制,到時候也會有錄屏,大家到時候看看就知道了,這里不細說。
簡單來說,就是獲取當前 token 的角色信息和訪問的URL地址,做匹配和判斷,判斷是否有權限,有,就 succeed,沒有就 failed(這里可能是 401 ,也可能是403)。
當沒有登錄的時候,就是 沒有登錄,或者token過期的時候,我們就 failed,會自動返回 401;
當token還有效,但是不匹配Role 和 URL 的時候,我們返回 failed,會自動返回 403 狀態碼;
這里截圖部分代碼,注意下,這里如果你之前寫其他返回內容了,要刪掉,只保留 failed 和 return:
但是,雖然是返回 401 和 403了,他們是這樣的,這種不好看,而且也沒有具體的響應 Message,不太友好
所以我們就需要自定義返回內容的格式。
2、定義響應實體類
我這里寫了一個很 low 的類,具體就是那個意思,大家看看即可,有更優雅的可以幫忙說說,或者提交個 PR:
namespace Blog.Core.AuthHelper.Policys { public class ApiResponse { public int Status { get; set; } = 404; public object Value { get; set; } = "No Found"; public ApiResponse(StatusCode apiCode) { switch (apiCode) { case StatusCode.CODE401: { Status = 401; Value = "很抱歉,您無權訪問該接口,請確保已經登錄!"; } break; case StatusCode.CODE403: { Status = 403; Value = "很抱歉,您的訪問權限等級不夠,聯系管理員!"; } break; } } } public enum StatusCode { CODE401, CODE403, CODE404, CODE500 } }
這個實體類,是用來返回響應內容的,如何使用,請往下看。
3、定義響應處理器
那我們既然自定義了響應內容,就需要定義響應處理器,方法就是繼承抽象類 AuthenticationHandler<TOptions> ,然后重寫方法:
namespace Blog.Core.AuthHelper { public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public ApiResponseHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { throw new NotImplementedException(); } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { Response.ContentType = "application/json"; Response.StatusCode = StatusCodes.Status401Unauthorized; await Response.WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE401))); } protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) { Response.ContentType = "application/json"; Response.StatusCode = StatusCodes.Status403Forbidden; await Response.WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE403))); } } }
這里很簡單,只是重寫了兩個異步方法,然后將內容 Response 出去即可。
4、替換默認Scheme方案
在上邊我們說到了,我們的認證服務 services.AddAuthentication() 它自己有一套返回格式和內容,就是上邊截圖的內容,那我們要修改,就需要給替換掉:
// 開啟Bearer認證 services.AddAuthentication(o=> { o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultChallengeScheme = nameof(ApiResponseHandler); o.DefaultForbidScheme = nameof(ApiResponseHandler); }) // 添加JwtBearer服務 .AddJwtBearer(o => { o.TokenValidationParameters = tokenValidationParameters; o.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { // 如果過期,則把<是否過期>添加到,返回頭信息中 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }) .AddScheme<AuthenticationSchemeOptions, ApiResponseHandler>(nameof(ApiResponseHandler), o => { });
這個大家要注意一下,我已經把 Starup 中的服務都提取出來了,一共十個,這樣大家在學習的時候,更方便。
到目前為止,我們就已經修改完成了,我們可以看看效果:
不僅使用了 HTTP 的 StatusCode 狀態碼,同時也可以自定義返回內容,兩個方案都兼容了,具體自己項目如何去使用,就看自己的需求了。
三、預告
這兩天重新開始寫 IdentityServer4 了,打算將我們的項目統一整合到 Ids 的授權服務中心里,同時也會錄一個視頻教程,因為 Blog.Core 的視頻教程已經完結,下一個是 Blog.IdentityServer 的視頻教程。
如果你有什么問題,或者疑問,或者想了解的,請留言評論。
四、Github && Gitee
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core