用JWT来保护我们的ASP.NET Core Web API
在上一篇博客中,自己动手写了一个Middleware来处理API的授权验证,现在就采用另外一种方式来处理这个授权验证的问题,毕竟现在也
有不少开源的东西可以用,今天用的是JWT。
什么是JWT呢?JWT的全称是JSON WEB TOKENS,是一种自包含令牌格式。官方网址:https://jwt.io/,或多或少应该都有听过这个。
先来看看下面的两个图:

就像左图展示的那样,发起了请求但是拿不到想要的结果;当站点先去授权服务器拿到了可以访问api的access_token(令牌)后,再通过这个
access_token去访问api,api才会返回受保护的数据资源。
这个就是基于令牌验证的大致流程了。可以看出授权服务器占着一个很重要的地位。
下面先来看看授权服务器做了些什么并如何来实现一个简单的授权。
做了什么?授权服务器在整个过程中的作用是:接收客户端发起申请access_token的请求,并校验其身份的合法性,最终返回一个包含
access_token的json字符串。
如何实现?我们还是离不开中间件这个东西。这次我们写了一个TokenProviderMiddleware,主要是看看invoke方法和生成access_token
的方法。
/// <summary>
/// invoke the middleware
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
await _next(context);
} // Request must be POST with Content-Type: application/x-www-form-urlencoded
if (!context.Request.Method.Equals("POST")
|| !context.Request.HasFormContentType)
{
await ReturnBadRequest(context);
}
await GenerateAuthorizedResult(context);
}
Invoke方法其实是不用多说的,不过我们这里是做了一个控制,只接收POST请求,并且是只接收以表单形式提交的数据,GET的请求和其
他contenttype类型是属于非法的请求,会返回bad request的状态。
下面说说授权中比较重要的东西,access_token的生成。
/// <summary>
/// get the jwt
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
private string GetJwt(string username)
{
var now = DateTime.UtcNow; var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(),
ClaimValueTypes.Integer64)
}; var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new
{
access_token = encodedJwt,
expires_in = (int)_options.Expiration.TotalSeconds,
token_type = "Bearer"
};
return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });
}
claims包含了多个claim,你想要那几个,可以根据自己的需要来添加,JwtRegisteredClaimNames是一个结构体,里面包含了所有的可选项。
public struct JwtRegisteredClaimNames
{
public const string Acr = "acr";
public const string Actort = "actort";
public const string Amr = "amr";
public const string AtHash = "at_hash";
public const string Aud = "aud";
public const string AuthTime = "auth_time";
public const string Azp = "azp";
public const string Birthdate = "birthdate";
public const string CHash = "c_hash";
public const string Email = "email";
public const string Exp = "exp";
public const string FamilyName = "family_name";
public const string Gender = "gender";
public const string GivenName = "given_name";
public const string Iat = "iat";
public const string Iss = "iss";
public const string Jti = "jti";
public const string NameId = "nameid";
public const string Nbf = "nbf";
public const string Nonce = "nonce";
public const string Prn = "prn";
public const string Sid = "sid";
public const string Sub = "sub";
public const string Typ = "typ";
public const string UniqueName = "unique_name";
public const string Website = "website";
}
JwtRegisteredClaimNames
还需要一个JwtSecurityToken对象,这个对象是至关重要的。有了时间、Claims和JwtSecurityToken对象,只要调用JwtSecurityTokenHandler
的WriteToken就可以得到类似这样的一个加密之后的字符串,这个字符串由3部分组成用‘.’分隔。每部分代表什么可以去官网查找。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最后我们要用json的形式返回这个access_token、access_token的有效时间和一些其他的信息。
还需要在Startup的Configure方法中去调用我们的中间件。
var audienceConfig = Configuration.GetSection("Audience");
var symmetricKeyAsBase64 = audienceConfig["Secret"];
var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
var signingKey = new SymmetricSecurityKey(keyByteArray);
app.UseTokenProvider(new TokenProviderOptions
{
Audience = "Catcher Wong",
Issuer = "http://catcher1994.cnblogs.com/",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
});
到这里,我们的授权服务站点已经是做好了。下面就编写几个单元测试来验证一下这个授权。
测试一:授权服务站点能生成正确的jwt。
[Fact]
public async Task authorized_server_should_generate_token_success()
{
//arrange
var data = new Dictionary<string, string>();
data.Add("username", "Member");
data.Add("password", "");
HttpContent ct = new FormUrlEncodedContent(data); //act
System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct);
string res = await message_token.Content.ReadAsStringAsync();
var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<Token>(res); //assert
Assert.NotNull(obj);
Assert.Equal("", obj.expires_in);
Assert.Equal(, obj.access_token.Split('.').Length);
Assert.Equal("Bearer", obj.token_type);
}
测试二:授权服务站点因为用户名或密码不正确导致不能生成正确的jwt。
[Fact]
public async Task authorized_server_should_generate_token_fault_by_invalid_app()
{
//arrange
var data = new Dictionary<string, string>();
data.Add("username", "Member");
data.Add("password", "");
HttpContent ct = new FormUrlEncodedContent(data); //act
System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct);
var res = await message_token.Content.ReadAsStringAsync();
dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res); //assert
Assert.Equal("invalid_grant", (string)obj.error);
Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode);
}
测试三:授权服务站点因为不是发起post请求导致不能生成正确的jwt。
[Fact]
public async Task authorized_server_should_generate_token_fault_by_invalid_httpmethod()
{
//arrange
Uri uri = new Uri("http://127.0.0.1:8000/auth/token?username=Member&password=123456"); //act
System.Net.Http.HttpResponseMessage message_token = await _client.GetAsync(uri);
var res = await message_token.Content.ReadAsStringAsync();
dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res); //assert
Assert.Equal("invalid_grant", (string)obj.error);
Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode);
}

断点拿一个access_token去http://jwt.calebb.net/ 解密看看
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNZW1iZXIiLCJqdGkiOiI2MzI1MmE1My0yMjY5LTQ4YzEtYmQwNi1lOWRiMzdmMTRmYTQiLCJpYXQiOiIyMDE2LzExLzEyIDI6NDg6MTciLCJuYmYiOjE0Nzg5MTg4OTcsImV4cCI6MTQ3ODkxOTQ5NywiaXNzIjoiaHR0cDovL2NhdGNoZXIxOTk0LmNuYmxvZ3MuY29tLyIsImF1ZCI6IkNhdGNoZXIgV29uZyJ9.Cu2vTJ4JAHgbJGzwv2jCmvz17HcyOsRnTjkTIEA0EbQ

下面就是API的开发了。
这里是直接用了新建API项目生成的ValueController作为演示,毕竟跟ASP.NET Web API是大同小异的。这里的重点是配置
JwtBearerAuthentication,这里是不用我们再写一个中间件了,我们是定义好要用的Option然后直接用JwtBearerAuthentication就可以了。
public void ConfigureJwtAuth(IApplicationBuilder app)
{
var audienceConfig = Configuration.GetSection("Audience");
var symmetricKeyAsBase64 = audienceConfig["Secret"];
var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
var signingKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(keyByteArray); var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey, // Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = "http://catcher1994.cnblogs.com/", // Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = "Catcher Wong", // Validate the token expiry
ValidateLifetime = true, ClockSkew = TimeSpan.Zero
}; app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters,
});
}
然后在Startup的Configure中调用上面的方法即可。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug(); ConfigureJwtAuth(app); app.UseMvc();
}
到这里之后,大部分的工作是已经完成了,还有最重要的一步,在想要保护的api上加上Authorize这个Attribute,这样Get这个方法就会要
求有access_token才会返回结果,不然就会返回401。这是在单个方法上的,也可以在整个控制器上面添加这个Attribute,这样控制器里面的方
法就都会受到保护。
// GET api/values/5
[HttpGet("{id}")]
[Authorize]
public string Get(int id)
{
return "value";
}
OK,同样编写几个单元测试验证一下。
测试一:valueapi在没有授权的请求会返回401状态。
[Fact]
public void value_api_should_return_unauthorized_without_auth()
{
//act
HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result;
string result = message.Content.ReadAsStringAsync().Result; //assert
Assert.False(message.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.Unauthorized,message.StatusCode);
Assert.Empty(result);
}
测试二:valueapi请求没有[Authorize]标记的方法时能正常返回结果。
[Fact]
public void value_api_should_return_result_without_authorize_attribute()
{
//act
HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values").Result;
string result = message.Content.ReadAsStringAsync().Result;
var res = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(result); //assert
Assert.True(message.IsSuccessStatusCode);
Assert.Equal(, res.Length);
}
测试三:valueapi在授权的请求中会返回正确的结果。
[Fact]
public void value_api_should_success_by_valid_auth()
{
//arrange
var data = new Dictionary<string, string>();
data.Add("username", "Member");
data.Add("password", "");
HttpContent ct = new FormUrlEncodedContent(data); //act
var obj = GetAccessToken(ct);
_client.DefaultRequestHeaders.Add("Authorization", "Bearer " + obj.access_token);
HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result;
string result = message.Content.ReadAsStringAsync().Result; //assert
Assert.True(message.IsSuccessStatusCode);
Assert.Equal(, obj.access_token.Split('.').Length);
Assert.Equal("value",result);
}
再来看看测试的结果:

再通过浏览器直接访问那个受保护的方法。响应头就会提示www-authenticate:Bearer,这个是身份验证的质询,告诉客户端必须要提供相
应的身份验证才能访问这个资源(api)。

这也是为什么在单元测试中会添加一个Header的原因,正常的使用也是要在请求的报文头中加上这个。
_client.DefaultRequestHeaders.Add("Authorization", "Bearer " + obj.access_token);
其实看一下源码,更快知道为什么。JwtBearerHandler.cs
用JWT来保护我们的ASP.NET Core Web API的更多相关文章
- 使用JWT创建安全的ASP.NET Core Web API
在本文中,你将学习如何在ASP.NET Core Web API中使用JWT身份验证.我将在编写代码时逐步简化.我们将构建两个终结点,一个用于客户登录,另一个用于获取客户订单.这些api将连接到在本地 ...
- Azure AD(二)调用受Microsoft 标识平台保护的 ASP.NET Core Web API 下
一,引言 上一节讲到如何在我们的项目中集成Azure AD 保护我们的API资源,以及在项目中集成Swagger,并且如何把Swagger作为一个客户端进行认证和授权去访问我们的WebApi资源的?本 ...
- ASP.NET Core Web API + Angular 仿B站(三)后台配置 JWT 的基于 token 的验证
前言: 本系列文章主要为对所学 Angular 框架的一次微小的实践,对 b站页面作简单的模仿. 本系列文章主要参考资料: 微软文档: https://docs.microsoft.com/zh-cn ...
- ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程
ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程 翻译自:地址 在今年年初,我整理了有关将JWT身份验证与ASP.NET Core Web API和Angular一起使用的详 ...
- ASP.NET Core Web API 索引 (更新Identity Server 4 视频教程)
GraphQL 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(上) 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(下) [视频] 使用ASP.NET C ...
- ASP.NET Core Web API 最佳实践指南
原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...
- 使用 Swagger 自动生成 ASP.NET Core Web API 的文档、在线帮助测试文档(ASP.NET Core Web API 自动生成文档)
对于开发人员来说,构建一个消费应用程序时去了解各种各样的 API 是一个巨大的挑战.在你的 Web API 项目中使用 Swagger 的 .NET Core 封装 Swashbuckle 可以帮助你 ...
- 在ASP.NET Core Web API上使用Swagger提供API文档
我在开发自己的博客系统(http://daxnet.me)时,给自己的RESTful服务增加了基于Swagger的API文档功能.当设置IISExpress的默认启动路由到Swagger的API文档页 ...
- Docker容器环境下ASP.NET Core Web API应用程序的调试
本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Docker容器环境下,对ASP.NET Core Web API应用程序进行调试.在 ...
随机推荐
- WebApi - 路由
这段时间的博客打算和大家一起分享下webapi的使用和心得,主要原因是群里面有朋友说希望能有这方面的文章分享,随便自己也再回顾下:后面将会和大家分不同篇章来分享交流心得,希望各位多多扫码支持和点赞,谢 ...
- 谈谈一些有趣的CSS题目(五)-- 单行居中,两行居左,超过两行省略
开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...
- mybatis_映射查询
一.一对一映射查询: 第一种方式(手动映射):借助resultType属性,定义专门的pojo类作为输出类型,其中该po类中封装了查询结果集中所有的字段.此方法较为简单,企业中使用普遍. <!- ...
- Angular2开发笔记
Problem 使用依赖注入应该注意些什么 服务一般用来做什么 指令一般用来做什么 angular2如何提取公共组件 angular2为什么不需要提公共组件 父组件与子组件之间如何通讯 什么时候应该使 ...
- Velocity初探小结--velocity使用语法详解
做java开发的朋友一般对JSP是比较熟悉的,大部分人第一次学习开发View层都是使用JSP来进行页面渲染的,我们都知道JSP是可以嵌入java代码的,在远古时代,java程序员甚至在一个jsp页面上 ...
- Android事件分发机制浅谈(一)
---恢复内容开始--- 一.是什么 我们首先要了解什么是事件分发,通俗的讲就是,当一个触摸事件发生的时候,从一个窗口到一个视图,再到一个视图,直至被消费的过程. 二.做什么 在深入学习android ...
- Java模拟Windows的Event
场景 开发中遇到一个场景,业务操作会不定时的产生工作任务,这些工作任务需要放入到一个队列中,而另外会有一个线程一直检测这个队列,队列中有任务就从队列中取出并进行运算. 问题 业务场景倒是简单,只不过这 ...
- embedding mono实战笔录(一)
最近在给自己的服务器节点添加脚本功能,考虑到 执行性能.开发效率.调试效率.可维护性.严谨性 五大要素,最终选用C#作为脚本语言,并使用mono作为中间层,使其具备跨平台特性,以备具有在Windows ...
- Leetcode 笔记 113 - Path Sum II
题目链接:Path Sum II | LeetCode OJ Given a binary tree and a sum, find all root-to-leaf paths where each ...
- 更有效率的使用Visual Studio(二)
没想到上一篇文章有这么多人喜欢,多谢大家支持.继续- 很多比较通用的快捷键的默认设置其实是有一些缩写在里面的,这个估计也是MS帮助我们记忆.比如说注释代码的快捷键是Ctrl + E + C,我们如果知 ...