在上一篇博客中,自己动手写了一个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部分组成用‘.’分隔。每部分代表什么可以去官网查找。

  1. 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", "");
  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("", obj.expires_in);
  18. Assert.Equal(, 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", "");
  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/ 解密看看

  1. 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(, 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", "");
  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(, 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!!!
 

用JWT来保护我们的ASP.NET Core Web API的更多相关文章

  1. 使用JWT创建安全的ASP.NET Core Web API

    在本文中,你将学习如何在ASP.NET Core Web API中使用JWT身份验证.我将在编写代码时逐步简化.我们将构建两个终结点,一个用于客户登录,另一个用于获取客户订单.这些api将连接到在本地 ...

  2. Azure AD(二)调用受Microsoft 标识平台保护的 ASP.NET Core Web API 下

    一,引言 上一节讲到如何在我们的项目中集成Azure AD 保护我们的API资源,以及在项目中集成Swagger,并且如何把Swagger作为一个客户端进行认证和授权去访问我们的WebApi资源的?本 ...

  3. ASP.NET Core Web API + Angular 仿B站(三)后台配置 JWT 的基于 token 的验证

    前言: 本系列文章主要为对所学 Angular 框架的一次微小的实践,对 b站页面作简单的模仿. 本系列文章主要参考资料: 微软文档: https://docs.microsoft.com/zh-cn ...

  4. ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程

    ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程 翻译自:地址 在今年年初,我整理了有关将JWT身份验证与ASP.NET Core Web API和Angular一起使用的详 ...

  5. ASP.NET Core Web API 索引 (更新Identity Server 4 视频教程)

    GraphQL 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(上) 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(下) [视频] 使用ASP.NET C ...

  6. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  7. 使用 Swagger 自动生成 ASP.NET Core Web API 的文档、在线帮助测试文档(ASP.NET Core Web API 自动生成文档)

    对于开发人员来说,构建一个消费应用程序时去了解各种各样的 API 是一个巨大的挑战.在你的 Web API 项目中使用 Swagger 的 .NET Core 封装 Swashbuckle 可以帮助你 ...

  8. 在ASP.NET Core Web API上使用Swagger提供API文档

    我在开发自己的博客系统(http://daxnet.me)时,给自己的RESTful服务增加了基于Swagger的API文档功能.当设置IISExpress的默认启动路由到Swagger的API文档页 ...

  9. Docker容器环境下ASP.NET Core Web API应用程序的调试

    本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Docker容器环境下,对ASP.NET Core Web API应用程序进行调试.在 ...

随机推荐

  1. Ubuntu 16.10 安装byzanz截取动态效果图工具

    1.了解byzanz截取动态效果图工具 byzanz能制作文件小,清晰的GIF动态效果图,不足就是,目前只能通过输入命令方式来录制. byzanz主要的参数选项有: -d, --duration=SE ...

  2. CoreCRM 开发实录——想用国货不容易

    昨天(2016年12月29日)发了开始开发的文章.本来晚上准备在 Coding.NET 上添加几个任务开始搞起了.可是真的开始用的时候才发现:Coding.NET 的任务功能只针对私有的任务开放.我想 ...

  3. Android ViewPager打造3D画廊

    本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发. 网上有很多关于使用Gallery来打造3D画廊的博客,但是在关于Gallery的官方说法中表明: This cl ...

  4. Entity Framework 手动使用migration里面的up 和down方法。

    add-migration -IgnoreChanges 201606100717405_201606100645298_InitialCreate 执行这一句后 ,清空使用map生成的代码,个人不太 ...

  5. css3圆形百分比进度条的实现原理

    原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...

  6. 快速了解微信小程序的使用,一个根据小程序的框架开发的todos app

    微信官方已经开放微信小程序的官方文档和开发者工具.前两天都是在看相关的新闻来了解小程序该如何开发,这两天官方的文档出来之后,赶紧翻看了几眼,重点了解了一下文档中框架与组件这两个部分,然后根据简易教程, ...

  7. 似懂非懂的localStorage和sessionStorage

    一.区别 相信很多人都见过这两个关于HTML5的新名词!HTML5种的web storage包含两种存储方式:localStorage和sessionStorage,这两种方式存储的数据不会自动发给服 ...

  8. ionic第二坑——ionic 上拉菜单(ActionSheet)安卓样式坑

    闲话不说,先上图: 这是IOS上的显示效果,代码如下: HTML部分: <body ng-app="starter" ng-controller="actionsh ...

  9. MMORPG大型游戏设计与开发(攻击区域 扇形)

    距离上次发布已经有了很长一段时间,期间由于各种原因没有更新这方面的技术分享,在这里深表遗憾.在MMO或其他的游戏中,会有针对各种形状的计算,通常在攻击区域里不会很复杂,常见的为矩形.圆形.扇形.今天分 ...

  10. 学习笔记 :DrawText

    最近在做一个TStringGrid的自绘处理,在画文字处理上遇到了高度的计算问题.后来经过一段时间还是找到了一些方法: 1.使用TLabel 这个方法是有点绕路的,方法倒是简单,就是使用AutoSiz ...