在上一篇博客中,本身動手寫了一個Middleware來處理API的受權驗證,如今就採用另一種方式來處理這個受權驗證的問題,畢竟如今也html
有很多開源的東西能夠用,今天用的是JWT。git
什麼是JWT呢?JWT的全稱是JSON WEB TOKENS,是一種自包含令牌格式。官方網址:https://jwt.io/,或多或少應該都有聽過這個。github
先來看看下面的兩個圖:web
就像左圖展現的那樣,發起了請求可是拿不到想要的結果;當站點先去受權服務器拿到了能夠訪問api的access_token(令牌)後,再經過這個json
access_token去訪問api,api纔會返回受保護的數據資源。api
這個就是基於令牌驗證的大體流程了。能夠看出受權服務器佔着一個很重要的地位。瀏覽器
下面先來看看受權服務器作了些什麼並如何來實現一個簡單的受權。服務器
作了什麼?受權服務器在整個過程當中的做用是:接收客戶端發起申請access_token的請求,並校驗其身份的合法性,最終返回一個包含app
access_token的json字符串。async
如何實現?咱們仍是離不開中間件這個東西。此次咱們寫了一個TokenProviderMiddleware,主要是看看invoke方法和生成access_token
的方法。
1 /// <summary> 2 /// invoke the middleware 3 /// </summary> 4 /// <param name="context"></param> 5 /// <returns></returns> 6 public async Task Invoke(HttpContext context) 7 { 8 if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) 9 { 10 await _next(context); 11 } 12 13 // Request must be POST with Content-Type: application/x-www-form-urlencoded 14 if (!context.Request.Method.Equals("POST") 15 || !context.Request.HasFormContentType) 16 { 17 await ReturnBadRequest(context); 18 } 19 await GenerateAuthorizedResult(context); 20 }
Invoke方法實際上是不用多說的,不過咱們這裏是作了一個控制,只接收POST請求,而且是隻接收以表單形式提交的數據,GET的請求和其
他contenttype類型是屬於非法的請求,會返回bad request的狀態。
下面說說受權中比較重要的東西,access_token的生成。
1 /// <summary> 2 /// get the jwt 3 /// </summary> 4 /// <param name="username"></param> 5 /// <returns></returns> 6 private string GetJwt(string username) 7 { 8 var now = DateTime.UtcNow; 9 10 var claims = new Claim[] 11 { 12 new Claim(JwtRegisteredClaimNames.Sub, username), 13 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 14 new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), 15 ClaimValueTypes.Integer64) 16 }; 17 18 var jwt = new JwtSecurityToken( 19 issuer: _options.Issuer, 20 audience: _options.Audience, 21 claims: claims, 22 notBefore: now, 23 expires: now.Add(_options.Expiration), 24 signingCredentials: _options.SigningCredentials); 25 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 26 27 var response = new 28 { 29 access_token = encodedJwt, 30 expires_in = (int)_options.Expiration.TotalSeconds, 31 token_type = "Bearer" 32 }; 33 return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }); 34 }
claims包含了多個claim,你想要那幾個,能夠根據本身的須要來添加,JwtRegisteredClaimNames是一個結構體,裏面包含了全部的可選項。
1 public struct JwtRegisteredClaimNames 2 { 3 public const string Acr = "acr"; 4 public const string Actort = "actort"; 5 public const string Amr = "amr"; 6 public const string AtHash = "at_hash"; 7 public const string Aud = "aud"; 8 public const string AuthTime = "auth_time"; 9 public const string Azp = "azp"; 10 public const string Birthdate = "birthdate"; 11 public const string CHash = "c_hash"; 12 public const string Email = "email"; 13 public const string Exp = "exp"; 14 public const string FamilyName = "family_name"; 15 public const string Gender = "gender"; 16 public const string GivenName = "given_name"; 17 public const string Iat = "iat"; 18 public const string Iss = "iss"; 19 public const string Jti = "jti"; 20 public const string NameId = "nameid"; 21 public const string Nbf = "nbf"; 22 public const string Nonce = "nonce"; 23 public const string Prn = "prn"; 24 public const string Sid = "sid"; 25 public const string Sub = "sub"; 26 public const string Typ = "typ"; 27 public const string UniqueName = "unique_name"; 28 public const string Website = "website"; 29 }
還須要一個JwtSecurityToken對象,這個對象是相當重要的。有了時間、Claims和JwtSecurityToken對象,只要調用JwtSecurityTokenHandler
的WriteToken就能夠獲得相似這樣的一個加密以後的字符串,這個字符串由3部分組成用‘.’分隔。每部分表明什麼能夠去官網查找。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最後咱們要用json的形式返回這個access_token、access_token的有效時間和一些其餘的信息。
還須要在Startup的Configure方法中去調用咱們的中間件。
1 var audienceConfig = Configuration.GetSection("Audience"); 2 var symmetricKeyAsBase64 = audienceConfig["Secret"]; 3 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); 4 var signingKey = new SymmetricSecurityKey(keyByteArray); 5 6 app.UseTokenProvider(new TokenProviderOptions 7 { 8 Audience = "Catcher Wong", 9 Issuer = "http://catcher1994.cnblogs.com/", 10 SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256), 11 });
到這裏,咱們的受權服務站點已是作好了。下面就編寫幾個單元測試來驗證一下這個受權。
測試一:受權服務站點能生成正確的jwt。
1 [Fact] 2 public async Task authorized_server_should_generate_token_success() 3 { 4 //arrange 5 var data = new Dictionary<string, string>(); 6 data.Add("username", "Member"); 7 data.Add("password", "123"); 8 HttpContent ct = new FormUrlEncodedContent(data); 9 10 //act 11 System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct); 12 string res = await message_token.Content.ReadAsStringAsync(); 13 var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<Token>(res); 14 15 //assert 16 Assert.NotNull(obj); 17 Assert.Equal("600", obj.expires_in); 18 Assert.Equal(3, obj.access_token.Split('.').Length); 19 Assert.Equal("Bearer", obj.token_type); 20 }
測試二:受權服務站點由於用戶名或密碼不正確致使不能生成正確的jwt。
1 [Fact] 2 public async Task authorized_server_should_generate_token_fault_by_invalid_app() 3 { 4 //arrange 5 var data = new Dictionary<string, string>(); 6 data.Add("username", "Member"); 7 data.Add("password", "123456"); 8 HttpContent ct = new FormUrlEncodedContent(data); 9 10 //act 11 System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct); 12 var res = await message_token.Content.ReadAsStringAsync(); 13 dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res); 14 15 //assert 16 Assert.Equal("invalid_grant", (string)obj.error); 17 Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode); 18 }
測試三:受權服務站點由於不是發起post請求致使不能生成正確的jwt。
1 [Fact] 2 public async Task authorized_server_should_generate_token_fault_by_invalid_httpmethod() 3 { 4 //arrange 5 Uri uri = new Uri("http://127.0.0.1:8000/auth/token?username=Member&password=123456"); 6 7 //act 8 System.Net.Http.HttpResponseMessage message_token = await _client.GetAsync(uri); 9 var res = await message_token.Content.ReadAsStringAsync(); 10 dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res); 11 12 //assert 13 Assert.Equal("invalid_grant", (string)obj.error); 14 Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode); 15 }
斷點拿一個access_token去http://jwt.calebb.net/ 解密看看
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNZW1iZXIiLCJqdGkiOiI2MzI1MmE1My0yMjY5LTQ4YzEtYmQwNi1lOWRiMzdmMTRmYTQiLCJpYXQiOiIyMDE2LzExLzEyIDI6NDg6MTciLCJuYmYiOjE0Nzg5MTg4OTcsImV4cCI6MTQ3ODkxOTQ5NywiaXNzIjoiaHR0cDovL2NhdGNoZXIxOTk0LmNuYmxvZ3MuY29tLyIsImF1ZCI6IkNhdGNoZXIgV29uZyJ9.Cu2vTJ4JAHgbJGzwv2jCmvz17HcyOsRnTjkTIEA0EbQ
下面就是API的開發了。
這裏是直接用了新建API項目生成的ValueController做爲演示,畢竟跟ASP.NET Web API是大同小異的。這裏的重點是配置
JwtBearerAuthentication,這裏是不用咱們再寫一箇中間件了,咱們是定義好要用的Option而後直接用JwtBearerAuthentication就能夠了。
1 public void ConfigureJwtAuth(IApplicationBuilder app) 2 { 3 var audienceConfig = Configuration.GetSection("Audience"); 4 var symmetricKeyAsBase64 = audienceConfig["Secret"]; 5 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); 6 var signingKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(keyByteArray); 7 8 var tokenValidationParameters = new TokenValidationParameters 9 { 10 // The signing key must match! 11 ValidateIssuerSigningKey = true, 12 IssuerSigningKey = signingKey, 13 14 // Validate the JWT Issuer (iss) claim 15 ValidateIssuer = true, 16 ValidIssuer = "http://catcher1994.cnblogs.com/", 17 18 // Validate the JWT Audience (aud) claim 19 ValidateAudience = true, 20 ValidAudience = "Catcher Wong", 21 22 // Validate the token expiry 23 ValidateLifetime = true, 24 25 ClockSkew = TimeSpan.Zero 26 }; 27 28 app.UseJwtBearerAuthentication(new JwtBearerOptions 29 { 30 AutomaticAuthenticate = true, 31 AutomaticChallenge = true, 32 TokenValidationParameters = tokenValidationParameters, 33 }); 34 }
而後在Startup的Configure中調用上面的方法便可。
1 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 2 { 3 loggerFactory.AddConsole(Configuration.GetSection("Logging")); 4 loggerFactory.AddDebug(); 5 6 ConfigureJwtAuth(app); 7 8 app.UseMvc(); 9 }
到這裏以後,大部分的工做是已經完成了,還有最重要的一步,在想要保護的api上加上Authorize這個Attribute,這樣Get這個方法就會要
求有access_token纔會返回結果,否則就會返回401。這是在單個方法上的,也能夠在整個控制器上面添加這個Attribute,這樣控制器裏面的方
法就都會受到保護。
1 // GET api/values/5 2 [HttpGet("{id}")] 3 [Authorize] 4 public string Get(int id) 5 { 6 return "value"; 7 }
OK,一樣編寫幾個單元測試驗證一下。
測試一:valueapi在沒有受權的請求會返回401狀態。
1 [Fact] 2 public void value_api_should_return_unauthorized_without_auth() 3 { 4 //act 5 HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result; 6 string result = message.Content.ReadAsStringAsync().Result; 7 8 //assert 9 Assert.False(message.IsSuccessStatusCode); 10 Assert.Equal(HttpStatusCode.Unauthorized,message.StatusCode); 11 Assert.Empty(result); 12 }
測試二:valueapi請求沒有[Authorize]標記的方法時能正常返回結果。
1 [Fact] 2 public void value_api_should_return_result_without_authorize_attribute() 3 { 4 //act 5 HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values").Result; 6 string result = message.Content.ReadAsStringAsync().Result; 7 var res = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(result); 8 9 //assert 10 Assert.True(message.IsSuccessStatusCode); 11 Assert.Equal(2, res.Length); 12 }
測試三:valueapi在受權的請求中會返回正確的結果。
1 [Fact] 2 public void value_api_should_success_by_valid_auth() 3 { 4 //arrange 5 var data = new Dictionary<string, string>(); 6 data.Add("username", "Member"); 7 data.Add("password", "123"); 8 HttpContent ct = new FormUrlEncodedContent(data); 9 10 //act 11 var obj = GetAccessToken(ct); 12 _client.DefaultRequestHeaders.Add("Authorization", "Bearer " + obj.access_token); 13 HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result; 14 string result = message.Content.ReadAsStringAsync().Result; 15 16 //assert 17 Assert.True(message.IsSuccessStatusCode); 18 Assert.Equal(3, obj.access_token.Split('.').Length); 19 Assert.Equal("value",result); 20 }
再來看看測試的結果:
再經過瀏覽器直接訪問那個受保護的方法。響應頭就會提示www-authenticate:Bearer,這個是身份驗證的質詢,告訴客戶端必需要提供相
應的身份驗證才能訪問這個資源(api)。
這也是爲何在單元測試中會添加一個Header的緣由,正常的使用也是要在請求的報文頭中加上這個。
_client.DefaultRequestHeaders.Add("Authorization", "Bearer " + obj.access_token);
其實看一下源碼,更快知道爲何。JwtBearerHandler.cs