在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
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 } 为什么要实现这个拓展方法呢?个人认为 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 全部评论
专题导读
热门推荐
热门话题
阅读排行榜
|
请发表评论