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,不进行任何的权限过滤。

 using Dapper;
using Microsoft.AspNetCore.Mvc;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using WebApi.CommandText;
using WebApi.Common;
using Common; namespace WebApi.Controllers
{
[Route("api/[controller]")]
public class BookController : Controller
{ private DapperHelper _helper;
public BookController(DapperHelper helper)
{
this._helper = helper;
} // GET: api/book
[HttpGet]
public async Task<IActionResult> Get()
{
var res = await _helper.QueryAsync(BookCommandText.GetBooks);
CommonResult<Book> json = new CommonResult<Book>
{
Code = "",
Message = "ok",
Data = res
};
return Ok(json);
} // GET api/book/5
[HttpGet("{id}")]
public IActionResult Get(int id)
{
DynamicParameters dp = new DynamicParameters();
dp.Add("@Id", id, DbType.Int32, ParameterDirection.Input);
var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();
CommonResult<Book> json = new CommonResult<Book>
{
Code = "",
Message = "ok",
Data = res
};
return Ok(json);
} // POST api/book
[HttpPost]
public IActionResult Post([FromForm]PostForm form)
{
DynamicParameters dp = new DynamicParameters();
dp.Add("@Id", form.Id, DbType.Int32, ParameterDirection.Input);
var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();
CommonResult<Book> json = new CommonResult<Book>
{
Code = "",
Message = "ok",
Data = res
};
return Ok(json);
} } public class PostForm
{
public string Id { get; set; }
} }
  api这边应该没什么好说的,都是一些常规的操作,会MVC的应该都可以懂。主要是根据id获取图书信息的方法(GET和POST)。这是我们后

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

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

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

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

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

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

 namespace WebApi.Middlewares
{
public class ApiAuthorizedOptions
{
//public string Name { get; set; } public string EncryptKey { get; set; } public int ExpiredSecond { get; set; }
}
}

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

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

   "ApiKey": {
//"username": "123",
//"password": "123",
"EncryptKey": "@*api#%^@",
"ExpiredSecond": ""
}

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

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

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

  1.参数是否被篡改

  2.请求是否已经过期

  3.请求的应用是否合法

  主检查方法:Check
          /// <summary>
/// the main check method
/// </summary>
/// <param name="context"></param>
/// <param name="requestInfo"></param>
/// <returns></returns>
private async Task Check(HttpContext context, RequestInfo requestInfo)
{
string computeSinature = HMACMD5Helper.GetEncryptResult($"{requestInfo.ApplicationId}-{requestInfo.Timestamp}-{requestInfo.Nonce}", _options.EncryptKey);
double tmpTimestamp;
if (computeSinature.Equals(requestInfo.Sinature) &&
double.TryParse(requestInfo.Timestamp, out tmpTimestamp))
{
if (CheckExpiredTime(tmpTimestamp, _options.ExpiredSecond))
{
await ReturnTimeOut(context);
}
else
{
await CheckApplication(context, requestInfo.ApplicationId, requestInfo.ApplicationPassword);
}
}
else
{
await ReturnNoAuthorized(context);
}
}

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

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

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

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

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

          /// <summary>
/// check the expired time
/// </summary>
/// <param name="timestamp"></param>
/// <param name="expiredSecond"></param>
/// <returns></returns>
private bool CheckExpiredTime(double timestamp, double expiredSecond)
{
double now_timestamp = (DateTime.UtcNow - new DateTime(, , )).TotalSeconds;
return (now_timestamp - timestamp) > expiredSecond;
}

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

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

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

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

有权限的应用程序。

         /// <summary>
/// check the application
/// </summary>
/// <param name="context"></param>
/// <param name="applicationId"></param>
/// <param name="applicationPassword"></param>
/// <returns></returns>
private async Task CheckApplication(HttpContext context, string applicationId, string applicationPassword)
{
var application = GetAllApplications().Where(x => x.ApplicationId == applicationId).FirstOrDefault();
if (application != null)
{
if (application.ApplicationPassword != applicationPassword)
{
await ReturnNoAuthorized(context);
}
}
else
{
await ReturnNoAuthorized(context);
}
}

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

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

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

  没有授权的返回方法ReturnNoAuthorized

         /// <summary>
/// not authorized request
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task ReturnNoAuthorized(HttpContext context)
{
BaseResponseResult response = new BaseResponseResult
{
Code = "",
Message = "You are not authorized!"
};
context.Response.StatusCode = ;
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}

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

  超时的返回方法ReturnTimeOut

         /// <summary>
/// timeout request
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task ReturnTimeOut(HttpContext context)
{
BaseResponseResult response = new BaseResponseResult
{
Code = "",
Message = "Time Out!"
};
context.Response.StatusCode = ;
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}

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

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

  HTTP GET请求的处理方法GetInvoke

         /// <summary>
/// http get invoke
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task GetInvoke(HttpContext context)
{
var queryStrings = context.Request.Query;
RequestInfo requestInfo = new RequestInfo
{
ApplicationId = queryStrings["applicationId"].ToString(),
ApplicationPassword = queryStrings["applicationPassword"].ToString(),
Timestamp = queryStrings["timestamp"].ToString(),
Nonce = queryStrings["nonce"].ToString(),
Sinature = queryStrings["signature"].ToString()
};
await Check(context, requestInfo);
}

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

这个请求的合法性。

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

         /// <summary>
/// http post invoke
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task PostInvoke(HttpContext context)
{
var formCollection = context.Request.Form;
RequestInfo requestInfo = new RequestInfo
{
ApplicationId = formCollection["applicationId"].ToString(),
ApplicationPassword = formCollection["applicationPassword"].ToString(),
Timestamp = formCollection["timestamp"].ToString(),
Nonce = formCollection["nonce"].ToString(),
Sinature = formCollection["signature"].ToString()
};
await Check(context, requestInfo);
}

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

        public ApiAuthorizedMiddleware(RequestDelegate next, IOptions<ApiAuthorizedOptions> options)
{
this._next = next;
this._options = options.Value;
} public async Task Invoke(HttpContext context)
{
switch (context.Request.Method.ToUpper())
{
case "POST":
if (context.Request.HasFormContentType)
{
await PostInvoke(context);
}
else
{
await ReturnNoAuthorized(context);
}
break;
case "GET":
await GetInvoke(context);
break;
default:
await GetInvoke(context);
break;
}
await _next.Invoke(context);
}

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

 using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
using System; namespace WebApi.Middlewares
{
public static class ApiAuthorizedExtensions
{
public static IApplicationBuilder UseApiAuthorized(this IApplicationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} return builder.UseMiddleware<ApiAuthorizedMiddleware>();
} public static IApplicationBuilder UseApiAuthorized(this IApplicationBuilder builder, ApiAuthorizedOptions options)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} if (options == null)
{
throw new ArgumentNullException(nameof(options));
} return builder.UseMiddleware<ApiAuthorizedMiddleware>(Options.Create(options));
}
}
}

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

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

 using Microsoft.Extensions.DependencyInjection;
using System; namespace WebApi.Middlewares
{
public static class ApiAuthorizedServicesExtensions
{ /// <summary>
/// Add response compression services.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <returns></returns>
public static IServiceCollection AddApiAuthorized(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
} return services;
} /// <summary>
/// Add response compression services and configure the related options.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="ResponseCompressionOptions"/>.</param>
/// <returns></returns>
public static IServiceCollection AddApiAuthorized(this IServiceCollection services, Action<ApiAuthorizedOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
} services.Configure(configureOptions);
return services;
}
}
}

ApiAuthorizedServicesExtensions

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

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

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

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

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

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

  下面是完整的Startup

 using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using WebApi.Common;
using WebApi.Middlewares; namespace WebApi
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
} builder.AddEnvironmentVariables();
Configuration = builder.Build();
} public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.Configure<IISOptions>(options =>
{ }); services.Configure<DapperOptions>(options =>
{
options.ConnectionString = Configuration.GetConnectionString("DapperConnection");
}); //api authorized middleware
services.AddApiAuthorized(options =>
{
options.EncryptKey = Configuration.GetSection("ApiKey")["EncryptKey"];
options.ExpiredSecond = Convert.ToInt32(Configuration.GetSection("ApiKey")["ExpiredSecond"]);
}); services.AddMvc(); services.AddSingleton<DapperHelper>();
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{ loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug(); app.UseDapper(); //api authorized middleware
app.UseApiAuthorized(); app.UseApplicationInsightsRequestTelemetry(); app.UseApplicationInsightsExceptionTelemetry(); app.UseMvc();
}
}
}

  万事具备,只欠测试!!

  建个类库项目,写个单元测试看看。

 using Common;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit; namespace WebApiTest
{
public class BookApiTest
{
private HttpClient _client;
private string applicationId = "";
private string applicationPassword = "";
private string timestamp = (DateTime.UtcNow - new DateTime(, , )).TotalSeconds.ToString();
private string nonce = new Random().Next(, ).ToString();
private string signature = string.Empty; public BookApiTest()
{
_client = new HttpClient();
_client.BaseAddress = new Uri("http://localhost:8091/");
_client.DefaultRequestHeaders.Clear();
signature = HMACMD5Helper.GetEncryptResult($"{applicationId}-{timestamp}-{nonce}", "@*api#%^@");
} [Fact]
public async Task book_api_get_by_id_should_success()
{
string queryString = $"applicationId={applicationId}&timestamp={timestamp}&nonce={nonce}&signature={signature}&applicationPassword={applicationPassword}"; HttpResponseMessage message = await _client.GetAsync($"api/book/4939?{queryString}");
var result = JsonConvert.DeserializeObject<CommonResult<Book>>(message.Content.ReadAsStringAsync().Result); Assert.Equal("", result.Code);
Assert.Equal(, result.Data.Id);
Assert.True(message.IsSuccessStatusCode);
} [Fact]
public async Task book_api_get_by_id_should_failure()
{
string inValidSignature = Guid.NewGuid().ToString();
string queryString = $"applicationId={applicationId}&timestamp={timestamp}&nonce={nonce}&signature={inValidSignature}&applicationPassword={applicationPassword}"; HttpResponseMessage message = await _client.GetAsync($"api/book/4939?{queryString}");
var result = JsonConvert.DeserializeObject<CommonResult<Book>>(message.Content.ReadAsStringAsync().Result); Assert.Equal("", result.Code);
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, message.StatusCode);
} [Fact]
public async Task book_api_post_by_id_should_success()
{
var data = new Dictionary<string, string>();
data.Add("applicationId", applicationId);
data.Add("applicationPassword", applicationPassword);
data.Add("timestamp", timestamp);
data.Add("nonce", nonce);
data.Add("signature", signature);
data.Add("Id", "");
HttpContent ct = new FormUrlEncodedContent(data); HttpResponseMessage message = await _client.PostAsync("api/book", ct);
var result = JsonConvert.DeserializeObject<CommonResult<Book>>(message.Content.ReadAsStringAsync().Result); Assert.Equal("", result.Code);
Assert.Equal(, result.Data.Id);
Assert.True(message.IsSuccessStatusCode); } [Fact]
public async Task book_api_post_by_id_should_failure()
{
string inValidSignature = Guid.NewGuid().ToString();
var data = new Dictionary<string, string>();
data.Add("applicationId", applicationId);
data.Add("applicationPassword", applicationPassword);
data.Add("timestamp", timestamp);
data.Add("nonce", nonce);
data.Add("signature", inValidSignature);
data.Add("Id", "");
HttpContent ct = new FormUrlEncodedContent(data); HttpResponseMessage message = await _client.PostAsync("api/book", ct);
var result = JsonConvert.DeserializeObject<CommonResult<Book>>(message.Content.ReadAsStringAsync().Result); Assert.Equal("", result.Code);
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, message.StatusCode);
}
}
}

  测试用的是XUnit。这里写了get和post的测试用例。

  下面来看看测试的效果。

  测试通过。这里是直接用VS自带的测试窗口来运行测试,比较直观。

  当然也可以通过我们的dotnet test命令来运行测试。

  本文的Demo已经上传到Github:

  https://github.com/hwqdt/Demos/tree/master/src/ASPNETCoreAPIAuthorizedDemo

  Thanks for your reading!

用Middleware给ASP.NET Core Web API添加自己的授权验证的更多相关文章

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

    本文转自:http://www.cnblogs.com/catcher1994/p/6021046.html Web API,是一个能让前后端分离.解放前后端生产力的好东西.不过大部分公司应该都没能做 ...

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

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

  3. List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac

    List多个字段标识过滤 class Program{  public static void Main(string[] args) { List<T> list = new List& ...

  4. C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志

    C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...

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

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

  6. 如何在ASP.NET Core Web API中使用Mini Profiler

    原文如何在ASP.NET Core Web API中使用Mini Profiler 由Anuraj发表于2019年11月25日星期一阅读时间:1分钟 ASPNETCoreMiniProfiler 这篇 ...

  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. Angular企业级开发(4)-ngResource和REST介绍

    一.RESTful介绍 RESTful维基百科 REST(表征性状态传输,Representational State Transfer)是Roy Fielding博士在2000年他的博士论文中提出来 ...

  2. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  3. Go web开发初探

    2017年的第一篇博客,也是第一次写博客,写的不好,请各位见谅. 本人之前一直学习java.java web,最近开始学习Go语言,所以也想了解一下Go语言中web的开发方式以及运行机制. 在< ...

  4. [原]Redis主从复制各种环境下测试

    Redis 主从复制各种环境下测试 测试环境: Linux ubuntu 3.11.0-12-generic 2GB Mem 1 core of Intel(R) Core(TM) i5-3470 C ...

  5. ASP.NET Core 中文文档目录

    翻译计划 五月中旬 .NET Core RC2 如期发布,我们遂决定翻译 ASP.NET Core 文档.我们在 何镇汐先生. 悲梦先生. 张仁建先生和 雷欧纳德先生的群中发布了翻译计划招募信息,并召 ...

  6. OpenLiveWriter代码插件

    1.OpenLiveWriter安装 Windows Live Writer在2012年就停止了更新,Open Live Writer(以下简称OLW)是由Windows Live WriterWri ...

  7. 6.在MVC中使用泛型仓储模式和依赖注入实现增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...

  8. Angular (SPA) WebPack模块化打包、按需加载解决方案完整实现

    文艺小说-?2F,言情小说-?3F,武侠小说-?9F long long ago time-1-1:A 使用工具,long long A ago time-1-2:A 使用分类工具,long long ...

  9. Dynamics CRM 之ADFS 使用 WID 的独立联合服务器

    ADFS 的使用 WID 的独立联合服务器适用于自己的测试环境,常用的就是在虚机中使用. 拓扑图如下: wID:联合身份验证服务配置为使用 Windows 内部数据库

  10. 好用的Markdown编辑器一览 readme.md 编辑查看

    https://github.com/pandao/editor.md https://pandao.github.io/editor.md/examples/index.html Editor.md ...