JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。html
JWT的官网地址:https://jwt.io/前端
通俗地来说,JWT是能表明用户身份的令牌,可使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。git
JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。github
受权:这是使用JWT的最多见方案。一旦用户登陆,每一个后续请求将包括JWT,容许用户访问该令牌容许的路由,服务和资源。Single Sign On是一种如今普遍使用JWT的功能,由于它的开销很小,而且可以在不一样的域中轻松使用。web
信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。由于JWT能够签名 - 例如,使用公钥/私钥对 - 您能够肯定发件人是他们所说的人。此外,因为使用标头和有效负载计算签名,您还能够验证内容是否未被篡改。算法
这种模式的问题在于,扩展性(scaling)很差。单机固然没有问题,若是是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都可以读取 session。若是session存储的节点挂了,那么整个服务都会瘫痪,体验至关很差,风险也很高。数据库
相比之下,JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登陆状态,这样服务就变成了无状态的,服务器集群也很好扩展。api
在紧凑的形式中,JSON Web Tokens由dot(.
)分隔的三个部分组成,它们是:跨域
所以,JWT一般以下所示:安全
xxxxx.yyyyy.zzzzz
标头一般由两部分组成:令牌的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。
例如:
{ "alg": "HS256", "typ": "JWT" }
而后,这个JSON被编码为Base64Url,造成JWT的第一部分。
Payload 部分也是一个 JSON 对象,用来存放实际须要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过时时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还能够在这个部分定义私有字段,下面就是一个例子。例如:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
注意,JWT 默认是不加密的,任何人均可以读到,因此不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature 部分是对前两部分的签名,防止数据篡改。
首先,须要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。而后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在此过程当中未被更改,而且,在使用私钥签名的令牌的状况下,它还能够验证JWT的发件人是不是它所声称的人。
把他们三个所有放在一块儿
输出是三个由点分隔的Base64-URL字符串,能够在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。
下面显示了一个JWT,它具备先前的头和有效负载编码,并使用机密签名。
若是您想使用JWT并将这些概念付诸实践,您可使用jwt.io Debugger来解码,验证和生成JWT。
在身份验证中,当用户使用其凭据成功登陆时,将返回JSON Web令牌。因为令牌是凭证,所以必须很是当心以防止出现安全问题。通常状况下,您不该该将令牌保留的时间超过要求。
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,一般在Authorization标头中。标题的内容应以下所示:
Authorization: Bearer <token>
在某些状况下,这能够是无状态受权机制。服务器的受保护路由将检查Authorization
标头中的有效JWT ,若是存在,则容许用户访问受保护资源。若是JWT包含必要的数据,则能够减小查询数据库以进行某些操做的须要,尽管可能并不是老是如此。
若是在标Authorization
头中发送令牌,则跨域资源共享(CORS)将不会成为问题,由于它不使用cookie。
下图显示了如何获取JWT并用于访问API或资源:
前面咱们介绍了JWT的原理,下面咱们在asp.net core实际项目中集成JWT。
首先咱们新建一个Demo asp.net core 空web项目
其中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" }; } } }
这里模拟一下登录校验,只验证了用户密码不为空即经过校验,真实环境完善校验用户和密码的逻辑。
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." }); } } } }
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?}"); }); } } }
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
咱们找一个趁手的工具,好比fiddler,而后把咱们的web站点运行起来
首先调用无权限的接口:http://localhost:5000/api/value1
正确地返回了数据,那么接下来咱们测试JWT的流程
首先咱们什么都不加调用接口:http://localhost:5000/api/value2
返回了状态码401,也就是未经受权:访问因为凭据无效被拒绝。 说明JWT校验生效了,咱们的接口收到了保护。
调用模拟登录受权接口:http://localhost:5000/api/Auth?userName=zhangsan&pwd=123
这里的用户密码是随便写的,由于咱们模拟登录只是校验了下非空,所以写什么都能经过
成功获得了响应
而后咱们获得了一个xxx.yyy.zzz 格式的 token 值。咱们把token复制出来
再次调用咱们的模拟数据接口,可是此次咱们加了一个HEADER:http://localhost:5000/api/value2
把内容粘出来
User-Agent: Fiddler Host: localhost:5000 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o
这里须要注意 Bearer 后面是有一个空格的,而后就是咱们上一步获取到的token
嗯,没有401了,成功返回了数据
咱们且倒一杯开水,坐等30分钟(咱们代码中设置的过时时间),而后再次调用数据接口:http://localhost:5000/api/value2
又变成了401,咱们看下详细的返回数据
这里有标注,错误描述 token过时,说明咱们设置的token过时时间生效了
假如咱们想在认证经过的时候,直接从jwt的token中获取到登录的用户名,该怎么操做呢?
首先在咱们的获取token 的api接口里面添加一个Claim节点,key能够随便给,也可使用已经提供好的一些预置Key,value是咱们登录的userName(仅做为演示)
而后在咱们的模拟数据接口获取自定义参数
这里使用HttpContext的受权扩展方法,拿到认证的信息,咱们来看下结果
请求成功返回,而且也拿到了咱们一开始写入的userName
token过时了说明登录信息已通过期,须要从新登录,跳转到登陆页从新登录获取新的token。(固然自动刷新token除外)
若是要保证token长期有效,能够前端在过时前调用登录接口刷新token。或者使用SignalR轮询,按期刷新token。
咱们有个ValidAudience(接收人),能够利用这个标准参数,登录时候生成一个GUID,在数据库/Redis/xxx存一份,而后验证接口的时候再把这个值拿出来去一块儿校验。若是值变了校验就失败了,固然,从新登录就会刷新这个值,因此只要从新登录,旧的token也就失效了。
当前Demo里面,咱们验证jwt的全部参数都是Const常量写死的,可是在真实生产环境都是能够走统一的配置中心,因此集群场景下,一个token能够在多个服务上被验证经过,由于校验token正确的密钥和相关参数都是从配置中心获取的。
到这里,咱们JWT的简介以及asp.net core 集成JWT已经完美完成,固然了这只是一个demo,在实际的应用中须要补充和完善的地方还有不少。
这一篇文章中评论区的一些疑问我放在了下一篇文章逐一解决,有兴趣的朋友请移步下文:asp.net core 集成JWT(二)token的强制失效,基于策略模式细化api权限
若是想要完整项目源码的,能够参考地址:https://github.com/sevenTiny/Demo.Jwt
若是有幸能帮助到你,高抬贵手点个star吧~