ASP.NET Core 集成 JWT

来自:7tiny

什么是JWT


JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。


JWT的官网地址:https://jwt.io/



信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。


JWT有什么优势?


先看我们传统的身份校验方式


  1. 用户向服务器发送用户名和密码。


  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。


  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。


  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。


  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。


这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。如果session存储的节点挂了,那么整个服务都会瘫痪,体验相当不好,风险也很高。


相比之下,JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。



1、Header 头


标头通常由两部分组成:令牌的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。


例如:


{
 "alg": "HS256",
 "typ": "JWT"
}


然后,这个JSON被编码为Base64Url,形成JWT的第一部分。



除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。例如:


{
 "sub": "1234567890",
 "name": "John Doe",
 "admin": true
}



HMACSHA256(
 base64UrlEncode(header) + "." +
 base64UrlEncode(payload),
 secret)





下图显示了如何获取JWT并用于访问API或资源:




添加数据访问模拟api,ValuesController


其中api/value1是可以直接访问的,api/value2添加了权限校验特性标签 [Authorize]


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Demo.Jwt.Controllers
{
   [ApiController]
   public class ValuesController : ControllerBase
   {
       [HttpGet]
       [Route("api/value1")]
       public ActionResult<IEnumerable<string>> Get()
       {
           return new string[] { "value1", "value1" };
       }

       [HttpGet]
       [Route("api/value2")]
       [Authorize]
       public ActionResult<IEnumerable<string>> Get2()
       {
           return new string[] { "value2", "value2" };
       }
   }
}


添加模拟登陆,生成Token的api,AuthController


这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。


using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace Demo.Jwt.Controllers
{
   [Route("api/[controller]")]
   [ApiController]
   public class AuthController : ControllerBase
   {
       [AllowAnonymous]
       [HttpGet]
       public IActionResult Get(string userName, string pwd)
       
{
           if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd))
           {
               var claims = new[]
               {
                   new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                   new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                   new Claim(ClaimTypes.Name, userName)

               };
               var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
               var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
               var token = new JwtSecurityToken(
                   issuer: Const.Domain,
                   audience: Const.Domain,
                   claims: claims,
                   expires: DateTime.Now.AddMinutes(30),
                   signingCredentials: creds);
               return Ok(new
               {
                   token = new JwtSecurityTokenHandler().WriteToken(token)
               });
           }
           else
           {
               return BadRequest(new { message = "username or password is incorrect." });

           }
       }
   }
}


Startup加JWT验证的相关配置


using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Text;

namespace Demo.Jwt
{
   public class Startup
   {
       public Startup(IConfiguration configuration)
       
{
           Configuration = configuration;
       }
       public IConfiguration Configuration { get; }
       // This method gets called by the runtime. Use this method to add services to the container.
       public void ConfigureServices(IServiceCollection services)
       
{
           //添加jwt验证:
         services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
               .AddJwtBearer(options => {
                   options.TokenValidationParameters = new TokenValidationParameters
                   {
                       ValidateIssuer = true,//是否验证Issuer
                       ValidateAudience = true,//是否验证Audience
                       ValidateLifetime = true,//是否验证失效时间
                       ClockSkew = TimeSpan.FromSeconds(30),

                       ValidateIssuerSigningKey = true,//是否验证SecurityKey
                       ValidAudience = Const.Domain,//Audience
                       ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致
                       IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                   };
               });
         services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
       }
       // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
       public void Configure(IApplicationBuilder app, IHostingEnvironment env)
       
{
           ///添加jwt验证
           app.UseAuthentication();
           if (env.IsDevelopment())
           {
               app.UseDeveloperExceptionPage();
           }
           app.UseMvc(routes =>
           {
               routes.MapRoute(
                   name: "default",
                       template: "{controller=Home}/{action=Index}/{id?}");
           });
       }
   }
}


最后把代码里面用到的一些相关常量也粘贴过来,Const.cs


namespace Demo.Jwt
{
   public class Const
   {
       /// <summary>
       /// 这里为了演示,写死一个密钥。实际生产环境可以从配置文件读取,这个是用网上工具随便生成的一个密钥
       /// </summary>
       public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB";
       public const string Domain = "http://localhost:5000";
   }
}


到这里,已经是我们项目的所有代码了。


完整的项目代码,Github地址:https://github.com/sevenTiny/Demo.Jwt






成功得到了响应





把内容粘出来


User-Agent: Fiddler
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o


这里需要注意 Bearer 后面是有一个空格的,然后就是我们上一步获取到的token



嗯,没有401了,成功返回了数据


4、JWT的Token过期


我们且倒一杯开水,坐等30分钟(我们代码中设置的过期时间),然后再次调用数据接口:http://localhost:5000/api/value2



又变成了401,我们看下详细的返回数据



这里有标注,错误描述 token过期,说明我们设置的token过期时间生效了


5、JWT添加自定义的参数(比如带上用户信息)


假如我们想在认证通过的时候,直接从jwt的token中获取到登陆的用户名,该怎么操作呢?


首先在我们的获取token 的api接口里面添加一个Claim节点,key可以随便给,也可以使用已经提供好的一些预置Key,value是我们登陆的userName(仅作为演示)


然后在我们的模拟数据接口获取自定义参数



这里使用HttpContext的授权扩展方法,拿到认证的信息,我们来看下结果




请求成功返回,并且也拿到了我们一开始写入的userName


结束


到这里,我们JWT的简介以及ASP.NET Core 集成JWT已经完美完成,当然了这只是一个demo,在实际的应用中需要补充和完善的地方还有很多。


完整项目源码地址:https://github.com/sevenTiny/Demo.Jwt

推荐↓↓↓
DotNet程序员
上一篇:C# 内存管理—职场生存的必修课 下一篇:ASP.NET Core 奇淫技巧之动态WebApi