1. 程式人生 > >使用請求頭認證來測試需要授權的 API 介面

使用請求頭認證來測試需要授權的 API 介面

# 使用請求頭認證來測試需要授權的 API 介面 ## Intro 有一些需要認證授權的介面在寫測試用例的時候一般會先獲取一個 token,然後再去呼叫介面,其實這樣做的話很不靈活,一方面是存在著一定的安全性問題,獲取 token 可能會有一些使用者名稱密碼之類的測試資料,還有就是獲取 token 的話如果全域性使用同一個 token 會很不靈活,如果我要測試沒有使用者資訊的話還比較簡單,我可以不傳遞 token,如果token裡有兩個角色,我要測試另外一個角色的時候,只能給這個測試使用者新增一個角色然後再獲取token,這樣就很不靈活,於是我就嘗試把之前寫的自定義請求頭認證的程式碼,整理了一下,整合到了一個 nuget 包裡以方便其他專案使用,nuget 包是 `WeihanLi.Web.Extensions`,原始碼在這裡 有想自己改的可以直接拿去用,目前提供了基於請求頭的認證和基於 QueryString 的認證兩種認證方式。 ## 實現效果 基於請求頭動態配置使用者的資訊,需要什麼樣的資訊就在請求頭中新增什麼資訊,示例如下: ![](https://img2020.cnblogs.com/blog/489462/202006/489462-20200609003002677-1623367090.png) ![](https://img2020.cnblogs.com/blog/489462/202006/489462-20200609003017882-1252450397.png) 再來看個單元測試的示例: ``` csharp [Fact] public async Task MakeReservationWithUserInfo() { using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations"); request.Headers.TryAddWithoutValidation("UserId", GuidIdGenerator.Instance.NewId()); // 使用者Id request.Headers.TryAddWithoutValidation("UserName", Environment.UserName); // 使用者名稱 request.Headers.TryAddWithoutValidation("UserRoles", "User,ReservationManager"); //使用者角色 request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""謝謝謝"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能廳"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json"); using var response = await Client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } ``` ## 實現原理解析 實現原理其實挺簡單的,就是實現了一種基於 header 的自定義認證模式,從 header 中獲取使用者資訊並進行認證,核心程式碼如下: ``` csharp protected override async Task HandleAuthenticateAsync() { if (await Options.AuthenticationValidator(Context)) { var claims = new List(); if (Request.Headers.TryGetValue(Options.UserIdHeaderName, out var userIdValues)) { claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString())); } if (Request.Headers.TryGetValue(Options.UserNameHeaderName, out var userNameValues)) { claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString())); } if (Request.Headers.TryGetValue(Options.UserRolesHeaderName, out var userRolesValues)) { var userRoles = userRolesValues.ToString() .Split(new[] { Options.Delimiter }, StringSplitOptions.RemoveEmptyEntries); claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r))); } if (Options.AdditionalHeaderToClaims.Count > 0) { foreach (var headerToClaim in Options.AdditionalHeaderToClaims) { if (Request.Headers.TryGetValue(headerToClaim.Key, out var headerValues)) { foreach (var val in headerValues.ToString().Split(new[] { Options.Delimiter }, StringSplitOptions.RemoveEmptyEntries)) { claims.Add(new Claim(headerToClaim.Value, val)); } } } } // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); var ticket = new AuthenticationTicket( principal, Scheme.Name ); return AuthenticateResult.Success(ticket); } return AuthenticateResult.NoResult(); } ``` 其實就是將請求頭的資訊讀取到 Claims,然後返回一個 `ClaimsPrincipal` 和 `AuthenticationTicket`,在讀取 header 之前有一個 `AuthenticationValidator` 是用來驗證請求是不是滿足使用 Header 認證,是一個基於 HttpContext 的斷言委託(`Func>`),預設實現是驗證是否有 UserId 對應的 Header,如果要修改可以通過 Startup 來配置 ## 使用示例 Startup 配置,和其它的認證方式一樣,Header 認證和 Query 認證也提供了基於 `AuthenticationBuilder` 的擴充套件,只需要在 `services.AddAuthentication()` 後增加 Header 認證的模式即可,示例如下: ``` csharp services.AddAuthentication(HeaderAuthenticationDefaults.AuthenticationSchema) .AddQuery(options => { options.UserIdQueryKey = "uid"; }) .AddHeader(options => { options.UserIdHeaderName = "X-UserId"; options.UserNameHeaderName = "X-UserName"; options.UserRolesHeaderName = "X-UserRoles"; }); ``` 預設的 Header 是 UserId/UserName/UserRoles,你也可以自定義為符合自己需要的配置,如果只是想新增一個轉換可以配置 `AdditionalHeaderToClaims` 增加自己需要的請求頭 => Claims 轉換,`AuthenticationValidator` 也可以自定義,就是上面提到的會首先會驗證是不是需要讀取 Header,驗證通過之後才會讀取 Header 資訊並認證 ## 測試示例 有一個介面我需要登入之後才能訪問,需要使用者資訊,類似下面這樣 ``` csharp [HttpPost] [Authorize] public async Task MakeReservation( [FromBody] ReservationViewModel model ) { // ... } ``` 在測試程式碼裡我配置使用了 Header 認證,在請求的時候直接通過 Header 來控制使用者的資訊 Startup 配置: ``` csharp services .AddAuthentication(HeaderAuthenticationDefaults.AuthenticationSchema) .AddHeader() // 使用 Query 認證 //.AddAuthentication(QueryAuthenticationDefaults.AuthenticationSchema) //.AddQuery() ; ``` 測試程式碼: ``` csharp [Fact] public async Task MakeReservationWithUserInfo() { using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations"); request.Headers.TryAddWithoutValidation("UserId", GuidIdGenerator.Instance.NewId()); request.Headers.TryAddWithoutValidation("UserName", Environment.UserName); request.Headers.TryAddWithoutValidation("UserRoles", "User,ReservationManager"); request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""謝謝謝"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能廳"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json"); using var response = await Client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task MakeReservationWithInvalidUserInfo() { using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations"); request.Headers.TryAddWithoutValidation("UserName", Environment.UserName); request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""謝謝謝"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能廳"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json"); using var response = await Client.SendAsync(request); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task MakeReservationWithoutUserInfo() { using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations") { Content = new StringContent( @"{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""謝謝謝"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能廳"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}", Encoding.UTF8, "application/json") }; using var response = await Client.SendAsync(request); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } ``` ## More QueryString 認證和請求頭認證是類似的,這裡就不再贅述,只是把請求頭上的引數轉移到 QueryString 上了,覺得不夠好用的可以直接 Github 上找原始碼修改, 也歡迎 PR,原始碼地址:
## Reference - - - - -