• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

用Middleware给ASP.NET Core Web API添加自己的授权验证

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

  Web API,是一个能让前后端分离、解放前后端生产力的好东西。不过大部分公司应该都没能做到完全的前后端分离。API的实现方式有很

多,可以用ASP.NET Core、也可以用ASP.NET Web API、ASP.NET MVC、NancyFx等。说到Web API,不同的人有不同的做法,可能前台、

中台和后台各一个api站点,也有可能一个模块一个api站点,也有可能各个系统共用一个api站点,当然这和业务有必然的联系。

  安全顺其自然的成为Web API关注的重点之一。现在流行的OAuth 2.0是个很不错的东西,不过本文是暂时没有涉及到的,只是按照最最最

原始的思路做的一个授权验证。在之前的MVC中,我们可能是通过过滤器来处理这个身份的验证,在Core中,我自然就是选择Middleware来处

理这个验证。

  下面开始本文的正题:

  先编写一个能正常运行的api,不进行任何的权限过滤。

 1 using Dapper;
 2 using Microsoft.AspNetCore.Mvc;
 3 using System.Data;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 using WebApi.CommandText;
 7 using WebApi.Common;
 8 using Common;
 9 
10 namespace WebApi.Controllers
11 {
12     [Route("api/[controller]")]
13     public class BookController : Controller
14     {
15 
16         private DapperHelper _helper;
17         public BookController(DapperHelper helper)
18         {
19             this._helper = helper;
20         }
21 
22         // GET: api/book
23         [HttpGet]
24         public async Task<IActionResult> Get()
25         {
26             var res = await _helper.QueryAsync(BookCommandText.GetBooks);
27             CommonResult<Book> json = new CommonResult<Book>
28             {
29                 Code = "000",
30                 Message = "ok",
31                 Data = res
32             };
33             return Ok(json);
34         }
35 
36         // GET api/book/5
37         [HttpGet("{id}")]
38         public IActionResult Get(int id)
39         {
40             DynamicParameters dp = new DynamicParameters();
41             dp.Add("@Id", id, DbType.Int32, ParameterDirection.Input);
42             var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();
43             CommonResult<Book> json = new CommonResult<Book>
44             {
45                 Code = "000",
46                 Message = "ok",
47                 Data = res
48             };
49             return Ok(json);
50         }
51 
52         // POST api/book        
53         [HttpPost]
54         public IActionResult Post([FromForm]PostForm form)
55         {
56             DynamicParameters dp = new DynamicParameters();
57             dp.Add("@Id", form.Id, DbType.Int32, ParameterDirection.Input);
58             var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();
59             CommonResult<Book> json = new CommonResult<Book>
60             {
61                 Code = "000",
62                 Message = "ok",
63                 Data = res
64             };
65             return Ok(json);
66         }
67 
68     }
69 
70     public class PostForm
71     {
72         public string Id { get; set; }
73     }
74 
75 }
  api这边应该没什么好说的,都是一些常规的操作,会MVC的应该都可以懂。主要是根据id获取图书信息的方法(GET和POST)。这是我们后

面进行单元测试的两个主要方法。这样部署得到的一个API站点,是任何一个人都可以访问http://yourapidomain.com/api/book 来得到相关

的数据。现在我们要对这个api进行一定的处理,让只有权限的站点才能访问它。

  下面就是编写自定义的授权验证中间件了。

  Middleware这个东西大家应该都不会陌生了,OWIN出来的时候就有中间件这样的概念了,这里就不展开说明,在ASP.NET Core中是如何

实现这个中间件的可以参考官方文档 Middleware。 

  我们先定义一个我们要用到的option,ApiAuthorizedOptions

 1 namespace WebApi.Middlewares
 2 {
 3     public class ApiAuthorizedOptions
 4     {
 5         //public string Name { get; set; }
 6 
 7         public string EncryptKey { get; set; }
 8         
 9         public int ExpiredSecond { get; set; }
10     }
11 }

  option内容比较简单,一个是EncryptKey ,用于对我们的请求参数进行签名,另一个是ExpiredSecond ,用于检验我们的请求是否超时。

与之对应的是在appsettings.json中设置的ApiKey节点

1   "ApiKey": {
2     //"username": "123",
3     //"password": "123",
4     "EncryptKey": "@*api#%^@",
5     "ExpiredSecond": "300"
6   }

  有了option,下面就可以编写middleware的内容了

  我们的api中就实现了get和post的方法,所以这里也就对get和post做了处理,其他http method,有需要的可以自己补充。

  这里的验证主要是下面的几个方面:

  1.参数是否被篡改

  2.请求是否已经过期

  3.请求的应用是否合法

  主检查方法:Check
 1          /// <summary>
 2         /// the main check method
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <param name="requestInfo"></param>
 6         /// <returns></returns>
 7         private async Task Check(HttpContext context, RequestInfo requestInfo)
 8         {
 9             string computeSinature = HMACMD5Helper.GetEncryptResult($"{requestInfo.ApplicationId}-{requestInfo.Timestamp}-{requestInfo.Nonce}", _options.EncryptKey);
10             double tmpTimestamp;
11             if (computeSinature.Equals(requestInfo.Sinature) &&
12                 double.TryParse(requestInfo.Timestamp, out tmpTimestamp))
13             {
14                 if (CheckExpiredTime(tmpTimestamp, _options.ExpiredSecond))
15                 {
16                     await ReturnTimeOut(context);
17                 }
18                 else
19                 {
20                     await CheckApplication(context, requestInfo.ApplicationId, requestInfo.ApplicationPassword);
21                 }
22             }
23             else
24             {
25                 await ReturnNoAuthorized(context);
26             }
27         }

  Check方法带了2个参数,一个是当前的httpcontext对象和请求的内容信息,当签名一致,并且时间戳能转化成double时才去校验是否超时

和Applicatioin的相关信息。这里的签名用了比较简单的HMACMD5加密,同样是可以换成SHA等加密来进行这一步的处理,加密的参数和规则是

随便定的,要有一个约定的过程,缺少灵活性(就像跟银行对接那样,银行说你就要这样传参数给我,不这样就不行,只好乖乖从命)。

  Check方法还用到了下面的4个处理

  1.子检查方法--超时判断CheckExpiredTime

 1          /// <summary>
 2         /// check the expired time
 3         /// </summary>
 4         /// <param name="timestamp"></param>
 5         /// <param name="expiredSecond"></param>
 6         /// <returns></returns>
 7         private bool CheckExpiredTime(double timestamp, double expiredSecond)
 8         {
 9             double now_timestamp = (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
10             return (now_timestamp - timestamp) > expiredSecond;
11         }

  这里取了当前时间与1970年1月1日的间隔与请求参数中传过来的时间戳进行比较,是否超过我们在appsettings中设置的那个值,超过就是

超时了,没超过就可以继续下一个步骤。

  2.子检查方法--应用程序判断CheckApplication

  应用程序要验证什么呢?我们会给每个应用程序创建一个ID和一个访问api的密码,所以我们要验证这个应用程序的真实身份,是否是那些

有权限的应用程序。

 1         /// <summary>
 2         /// check the application
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <param name="applicationId"></param>
 6         /// <param name="applicationPassword"></param>
 7         /// <returns></returns>
 8         private async Task CheckApplication(HttpContext context, string applicationId, string applicationPassword)
 9         {
10             var application = GetAllApplications().Where(x => x.ApplicationId == applicationId).FirstOrDefault();
11             if (application != null)
12             {
13                 if (application.ApplicationPassword != applicationPassword)
14                 {
15                     await ReturnNoAuthorized(context);
16                 }
17             }
18             else
19             {
20                 await ReturnNoAuthorized(context);
21             }
22         }

  先根据请求参数中的应用程序id去找到相应的应用程序,不能找到就说明不是合法的应用程序,能找到再去验证其密码是否正确,最后才确

定其能否取得api中的数据。

  下面两方法是处理没有授权和超时处理的实现:

  没有授权的返回方法ReturnNoAuthorized

 1         /// <summary>
 2         /// not authorized request
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <returns></returns>
 6         private async Task ReturnNoAuthorized(HttpContext context)
 7         {
 8             BaseResponseResult response = new BaseResponseResult
 9             {
10                 Code = "401",
11                 Message = "You are not authorized!"
12             };
13             context.Response.StatusCode = 401;
14             await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
15         }

  这里做的处理是将响应的状态码设置成401(Unauthorized)。

  超时的返回方法ReturnTimeOut

 1         /// <summary>
 2         /// timeout request 
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <returns></returns>
 6         private async Task ReturnTimeOut(HttpContext context)
 7         {
 8             BaseResponseResult response = new BaseResponseResult
 9             {
10                 Code = "408",
11                 Message = "Time Out!"
12             };
13             context.Response.StatusCode = 408;
14             await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
15         }

  这里做的处理是将响应的状态码设置成408(Time Out)。

  下面就要处理Http的GET请求和POST请求了。

  HTTP GET请求的处理方法GetInvoke

 1         /// <summary>
 2         /// http get invoke
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <returns></returns>
 6         private async Task GetInvoke(HttpContext context)
 7         {
 8             var queryStrings = context.Request.Query;
 9             RequestInfo requestInfo = new RequestInfo
10             {
11                 ApplicationId = queryStrings["applicationId"].ToString(),
12                 ApplicationPassword = queryStrings["applicationPassword"].ToString(),
13                 Timestamp = queryStrings["timestamp"].ToString(),
14                 Nonce = queryStrings["nonce"].ToString(),
15                 Sinature = queryStrings["signature"].ToString()
16             };
17             await Check(context, requestInfo);
18         }

  处理比较简单,将请求的参数赋值给RequestInfo,然后将当前的httpcontext和这个requestinfo交由我们的主检查方法Check去校验

这个请求的合法性。

  同理,HTTP POST请求的处理方法PostInvoke,也是同样的处理。

 1         /// <summary>
 2         /// http post invoke
 3         /// </summary>
 4         /// <param name="context"></param>
 5         /// <returns></returns>
 6         private async Task PostInvoke(HttpContext context)
 7         {
 8             var formCollection = context.Request.Form;
 9             RequestInfo requestInfo = new RequestInfo
10             {
11                 ApplicationId = formCollection["applicationId"].ToString(),
12                 ApplicationPassword = formCollection["applicationPassword"].ToString(),
13                 Timestamp = formCollection["timestamp"].ToString(),
14                 Nonce = formCollection["nonce"].ToString(),
15                 Sinature = formCollection["signature"].ToString()
16             };
17             await Check(context, requestInfo);
18         }

  最后是Middleware的构造函数和Invoke方法。

 1        public ApiAuthorizedMiddleware(RequestDelegate next, IOptions<ApiAuthorizedOptions> options)
 2         {
 3             this._next = next;
 4             this._options = options.Value;
 5         }
 6 
 7         public async Task Invoke(HttpContext context)
 8         {
 9             switch (context.Request.Method.ToUpper())
10             {
11                 case "POST":
12                     if (context.Request.HasFormContentType)
13                     {
14                         await PostInvoke(context);
15                     }
16                     else
17                     {
18                         await ReturnNoAuthorized(context);
19                     }
20                     break;
21                 case "GET":
22                     await GetInvoke(context);
23                     break;
24                 default:
25                     await GetInvoke(context);
26                     break;
27             }
28             await _next.Invoke(context);
29         }

  到这里,Middleware是已经编写好了,要在Startup中使用,还要添加一个拓展方法ApiAuthorizedExtensions

 1 using Microsoft.AspNetCore.Builder;
 2 using Microsoft.Extensions.Options;
 3 using System;
 4 
 5 namespace WebApi.Middlewares
 6 {
 7     public static class ApiAuthorizedExtensions
 8     {
 9         public static IApplicationBuilder UseApiAuthorized(this IApplicationBuilder builder)
10         {
11             if (builder == null)
12             {
13                 throw new ArgumentNullException(nameof(builder));
14             }
15 
16             return builder.UseMiddleware<ApiAuthorizedMiddleware>();
17         }
18 
19         public static IApplicationBuilder UseApiAuthorized(this IApplicationBuilder builder, ApiAuthorizedOptions options)
20         {
21             if (builder == null)
22             {
23                 throw new ArgumentNullException(nameof(builder));
24             }
25 
26             if (options == null)
27             {
28                 throw new ArgumentNullException(nameof(options));
29             }
30             
31             return builder.UseMiddleware<ApiAuthorizedMiddleware>(Options.Create(options));
32         }
33     }
34 }

  到这里我们已经可以在Startup的Configure和ConfigureServices方法中配置这个中间件了

  这里还有一个不一定非要实现的拓展方法ApiAuthorizedServicesExtensions,但我个人还是倾向于实现这个ServicesExtensions。

 1 using Microsoft.Extensions.DependencyInjection;
 2 using System;
 3 
 4 namespace WebApi.Middlewares
 5 {
 6     public static class ApiAuthorizedServicesExtensions
 7     {
 8 
 9         /// <summary>
10         /// Add response compression services.
11         /// </summary>
12         /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
13         /// <returns></returns>
14         public static IServiceCollection AddApiAuthorized(this IServiceCollection services)
15         {
16             if (services == null)
17             {
18                 throw new ArgumentNullException(nameof(services));
19             }
20 
21             return services;
22         }
23 
24         /// <summary>
25         /// Add response compression services and configure the related options.
26         /// </summary>
27         /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
28         /// <param name="configureOptions">A delegate to configure the <see cref="ResponseCompressionOptions"/>.</param>
29         /// <returns></returns>
30         public static IServiceCollection AddApiAuthorized(this IServiceCollection services, Action<ApiAuthorizedOptions> configureOptions)
31         {
32             if (services == null)
33             {
34                 throw new ArgumentNullException(nameof(services));
35             }
36             if (configureOptions == null)
37             {
38                 throw new ArgumentNullException(nameof(configureOptions));
39             }
40 
41             services.Configure(configureOptions);
42             return services;
43         }
44     }
45 }
ApiAuthorizedServicesExtensions

  为什么要实现这个拓展方法呢?个人认为

  Options、Middleware、Extensions、ServicesExtensions这四个是实现一个中间件的标配(除去简单到不行的那些中间件)

  Options给我们的中间件提供了一些可选的处理,提高了中间件的灵活性;

  Middleware是我们中间件最最重要的实现;

  Extensions是我们要在Startup的Configure去表明我们要使用这个中间件;

  ServicesExtensions是我们要在Startup的ConfigureServices去表明我们把这个中间件添加到容器中。

  下面是完整的Startup

 1 using Microsoft.AspNetCore.Builder;
 2 using Microsoft.AspNetCore.Hosting;
 3 using Microsoft.Extensions.Configuration;
 4 using Microsoft.Extensions.DependencyInjection;
 5  
                       
                    
                    

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
使用asp.net开发的一个东平人才网招聘程序发布时间:2022-07-10
下一篇:
ASP.NET 路由发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap