什么是JWT
JSON Web Token(JWT)是目前最流行的跨域身份验证、分布式登录、单点登录等解决方案。
JWT的官网地址:https://jwt.io/
通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。
JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JSON Web令牌能做什么?
- 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。Single Sign On是一种现在广泛使用JWT的功能,因为它的开销很小,并且能够在不同的域中轻松使用。
- 信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。
JSON Web令牌如何工作?
在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中,标题的内容应如下所示:
Authorization: Bearer <token>
在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization
标头中的有效JWT ,如果存在,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。
如果在标Authorization
头中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。
下图显示了如何获取JWT并用于访问API或资源:
1、应用程序向授权服务器请求授权;
2、校验用户身份,校验成功,返回token;
3、应用程序使用访问令牌访问受保护的资源。
JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。
更多理论知识可以查看官网,或者查看相关网友的文章,如下推荐文章:
- asp.net core 集成JWT(一):https://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
- 五分钟带你了解啥是JWT:https://zhuanlan.zhihu.com/p/86937325
- C#分布式登录——jwt:https://www.cnblogs.com/yswenli/p/13510050.html
net core 集成jwt代码实现
新建项目
首先我们新建一个ASP.NET Core Web API项目,命名为 jwtWebAPI,选择目标框架.NET Core3.1,注意,如果勾选了https配置,postman请求的时候要设置去除ssl认证才能使用,建议不配置https。
在nuget里面引用jwt集成的程序包,这里需要注意的是,如果你用的是.NET Core 3.1的框架的话,程序包版本选择3.1.10
Microsoft.AspNetCore.Authentication.JwtBearer
添加数据访问模拟api,新建控制器ValuesController
其中api/value1是可以直接访问的,api/value2添加了权限校验特性标签 [Authorize]
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace jwtWebAPI.Controllers { [ApiController] public class ValuesController : ControllerBase { [HttpGet] [Route("api/values1")] public ActionResult<IEnumerable<string>> values1() { return new string[] { "value1", "value1" }; } /** * 该接口用Authorize特性做了权限校验,如果没有通过权限校验,则http返回状态码为401 * 调用该接口的正确姿势是: * 1.登陆,调用api/Auth接口获取到token * 2.调用该接口 api/value2 在请求的Header中添加参数 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzM1MzM3IiwiZXhwIjoxNTYwMzM3MTM3LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.1S-40SrA4po2l4lB_QdzON_G5ZNT4P_6U25xhTcl7hI * Bearer后面有空格,且后面是第一步中接口返回的token值 * */ [HttpGet] [Route("api/value2")] [Authorize] public ActionResult<IEnumerable<string>> value2() { //这是获取自定义参数的方法 var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims; var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value; return new string[] { "访问成功:这个接口登陆过的用户都可以访问", $"userName={userName}" }; } } }
添加模拟登陆生成Token的api,新建控制器AuthController
这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; 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; namespace jwtWebAPI.Controllers { [ApiController] public class AuthController : Controller { /// <summary> /// 通过账号+密码获取Token /// </summary> /// <param name="userName"></param> /// <param name="pwd"></param> /// <returns>Token</returns> [AllowAnonymous] [HttpGet] [Route("api/auth")] public IActionResult GetToken(string userName, string pwd) { if (!string.IsNullOrEmpty(userName)) { //每次登陆动态刷新 Const.ValidAudience = userName + pwd + DateTime.Now.ToString(); // push the user’s name into a claim, so we can identify the user later on. //这里可以随意加入自定义的参数,key可以自己随便起 var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(3)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.NameIdentifier, userName) }; //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token. var token = new JwtSecurityToken( //颁发者 issuer: Const.Domain, //接收者 audience: Const.ValidAudience, //过期时间(可自行设定,注意和上面的claims内部Exp参数保持一致) expires: DateTime.Now.AddMinutes(3), //签名证书 signingCredentials: creds, //自定义参数 claims: claims ); 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.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace jwtWebAPI { 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 { ValidateLifetime = true,//是否验证失效时间 ClockSkew = TimeSpan.FromSeconds(30), //时间偏移量(允许误差时间) ValidateAudience = true,//是否验证Audience(验证之前的token是否失效) //ValidAudience = Const.GetValidudience(),//Audience //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了 AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//是否验证Issuer(颁发者) ValidAudience = Const.Domain,//Audience 【Const是新建的一个常量类】 接收者 ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致 颁发者 ValidateIssuerSigningKey = true,//是否验证SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { //Token expired if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { //添加jwt验证 app.UseAuthentication(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }