JSON WEB TOKENS
用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
的方法。

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是一个结构体,里面包含了所有的可选项。
还需要一个JwtSecurityToken对象,这个对象是至关重要的。有了时间、Claims和JwtSecurityToken对象,只要调用JwtSecurityTokenHandler
的WriteToken就可以得到类似这样的一个加密之后的字符串,这个字符串由3部分组成用‘.’分隔。每部分代表什么可以去官网查找。
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", "123");
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("600", obj.expires_in);
18 Assert.Equal(3, 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", "123456");
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/ 解密看看
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(2, 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", "123");
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(3, 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
JSON WEB TOKENS的更多相关文章
- [Node.js] Creating JWTs (JSON Web Tokens) in Node
		
In this lesson we will look at all of the pieces that combine together to create a JWT (j AWT) or JS ...
 - JSON Web Tokens(JWT)
		
现在API越来越流行,如何安全保护这些API? JSON Web Tokens(JWT)能提供基于JSON格式的安全认证.它有以下特点: JWT是跨不同语言的,JWT可以在 .NET, Python, ...
 - JSON Web Tokens测试工具
		
JSON Web Tokens官方提供测试工具https://jwt.io某些静态资料需要链接google.twitter服务器,被墙无法访问.现在提供可以方法测试工具http://hingtai.c ...
 - Implement JSON Web Tokens Authentication in ASP.NET Web API and Identity 2.1 Part 3 (by TAISEER)
		
http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-an ...
 - Java分布式:JWT(JSON Web Tokens)
		
Java分布式:JWT(JSON Web Tokens) 0.优势 Session方式存储用户状态占用大量服务器内存.一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存 ...
 - 服务安全-JWT(JSON Web Tokens):百科
		
ylbtech-服务安全-JWT(JSON Web Tokens):百科 JSON Web Tokens是一种开放的行业标准 RFC 7519方法,用于在双方之间安全地表示索赔. JWT.IO允许您解 ...
 - JWT & JSON Web Tokens
		
JSON Web Tokens https://jwt.io json web token example https://jwt.io/introduction/ https://medium.co ...
 - [翻译]Introduction to JSON Web Tokens
		
JWT: Json Web Tokens JWT是一种开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于将各方之间的信息安全地传输为JSON对象.因为它是经过数字签名的,所以该信息可以进 ...
 - ASP.NET Core 简单集成签发 JWT (JSON Web Tokens)
		
什么是 JWT ? 从 https://jwt.io/ 可以了解到对 JWT 的描述:JSON Web Tokens are an open, industry standard RFC 7519 m ...
 
随机推荐
- 十五、struts2中的拦截器(框架功能核心)
			
十五.struts2中的拦截器(框架功能核心) 1.过滤器VS拦截器 功能是一回事. 过滤器是Servlet规范中的技术,可以对请求和响应进行过滤. 拦截器是Struts2框架中的技术,实现AOP(面 ...
 - s3c6410_中断
			
参考: 1)<USER'S MANUAL-S3C6410X>Chapter 10 GPIO, Chapter 12 VECTORED INTERRUPT CONTROLLER 2)< ...
 - jquery中选择ID以什么字符开头的匹配主要用于多个上传控件的时候,id无法使用,而且class不起作用的时候
			
$("[id^=remark]")选择ID以remark开头的所有数据进行匹配
 - 破解金盘gdlisxp系统
			
1.现在要破解的金盘gdlisxp系统版本 2.首先在你电脑上要有脱壳工具AspackDie,和OllyDBG动态调试工具,电脑上装好金盘软件. 3.用AspackDie进行对金盘应用程序脱壳处理,生 ...
 - 在Entity Framework 中执行T-sql语句
			
从Entity Framework 4开始在ObjectContext对象上提供了2个方法可以直接执行SQL语句:ExecuteStoreQuery<T> 和 ExecuteStoreC ...
 - [leetcode]_Same Tree
			
第一次遇见Tree的题,拿到心慌,网上查了解题思路.写完就三行.. 最近努力学习一句话,学会喜欢自己. 题目:give two tree , you must judge if they are th ...
 - “requireJs前传”之为什么要用前端模块化?
			
对于没有接触过后台的前端同学想要理解模块化是很困难的,鉴于未来的趋势,以下是我转载的一篇文章,希望对大家有用! 特此声明:转载文章,不喜勿喷.和谐前端,世界和平!0.0 模块的写法 随着网站逐渐变成” ...
 - Nginx下10个安全问题提示
			
Nginx是当今最流行的Web服务器之一.它为世界上7%的web流量提供服务而且正在以惊人的速度增长.它是个让人惊奇的服务器,我愿意部署它 下面是一个常见安全陷阱和解决方案的列表,它可以辅助来确保你的 ...
 - php 解决json_encode中文问题
			
众所周知使用json_encode可以方便快捷地将对象进行json编码,但是如果对象的属性中存在着中文,问题也就随之而来了.json_encode会将中文转换为unicode编码例如:'胥'经过jso ...
 - linuxok6410的I2C驱动分析---用户态驱动
			
3 i2c-dev 3.1 概述 之前在介绍I2C子系统时,提到过使用i2c-dev.c文件在应用程序中实现我们的I2C从设备驱动.不过,它实现的是一个虚拟,临时的i2c_client,随着设备文件 ...