zoukankan      html  css  js  c++  java
  • 用JWT来保护我们的ASP.NET Core Web API

       在上一篇博客中,自己动手写了一个Middleware来处理API的授权验证,现在就采用另外一种方式来处理这个授权验证的问题,毕竟现在也

    有不少开源的东西可以用,今天用的是JWT。

      什么是JWT呢?JWT的全称是JSON WEB TOKENS,是一种自包含令牌格式。官方网址:https://jwt.io/,或多或少应该都有听过这个。

      先来看看下面的两个图:

      站点是通过RPC的方式来访问api取得资源的,当站点是直接访问api,没有拿到有访问权限的令牌,那么站点是拿不到相关的数据资源的。

    就像左图展示的那样,发起了请求但是拿不到想要的结果;当站点先去授权服务器拿到了可以访问api的access_token(令牌)后,再通过这个

    access_token去访问api,api才会返回受保护的数据资源。

      这个就是基于令牌验证的大致流程了。可以看出授权服务器占着一个很重要的地位。

      下面先来看看授权服务器做了些什么并如何来实现一个简单的授权。

      做了什么?授权服务器在整个过程中的作用是:接收客户端发起申请access_token的请求,并校验其身份的合法性,最终返回一个包含

    access_token的json字符串。

      如何实现?我们还是离不开中间件这个东西。这次我们写了一个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     }
    JwtRegisteredClaimNames

    还需要一个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

      下图是关于头部加Authorization的源码解释。
     
      
     
      本文的示例代码:JWTTokenDemo
     
     
      Thanks for your reading!!!
     
  • 相关阅读:
    poj3625
    Directx10,11的SwapChain,RenderTarget和DepthBuffer解释
    Everything you need to know about Authenticode Code Signing
    Best Render Settings for After Effects CS5
    javascript 中cookie的存储,获取cookie,删除cookie的方法
    白话数字签名(番外篇)——签名EXE文件(下)
    在php中使用CKEDITOR在线编辑器
    HTML5 VIDEO标签播放事件流水
    白话数字签名(番外篇)——签名EXE文件(上)
    自定义hint框
  • 原文地址:https://www.cnblogs.com/catcher1994/p/6057484.html
Copyright © 2011-2022 走看看