0、引言
若不清楚什么是JWT的请先了解下什么是JWT。
1、关于Authentication与Authorization
我相信在aspnet core中刚接触甚至用了段时间这两个概念的时候都是一头雾水的,傻傻分不清。
认证(Authentication)和授权(Authorization)在概念上比较的相似,且又有一定的联系,因此很容易混淆。
认证(Authentication)是指验证用户身份的过程,即当用户要访问受保护的资源时,将其信息(如用户名和密码)发送给服务器并由服务器验证的过程。
授权(Authorization)是验证一个已通过身份认证的用户是否有权限做某件事情的过程。
有过RBAC的开发经验者来说这里可以这么通俗的来理解:认证是验证一个用户是否“合法”(一般就是检查数据库中是否有这么个用户),授权是验证这个用户是否有做事情的权限(简单理解成RBAC中的用户权限)。
2、整个认证流程是怎样的?
从图中可以看到整个认证、授权的流程,先进行身份验证 ,验证通过后将Token放回给客户端,客户端访问资源的时候请求头中添加Token信息,服务器进行验证并于授权是否能够访问该资源。
3、开始JWT身份认证
3.1 引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer
3.2 在Startup.cs文件中进行配置
#region jwt验证 var jwtConfig = new JwtConfig(); Configuration.Bind("JwtConfig", jwtConfig); services .AddAuthentication(option => { //认证middleware配置 option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { //Token颁发机构 ValidIssuer = jwtConfig.Issuer, //颁发给谁 ValidAudience = jwtConfig.Audience, //这里的key要进行加密 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SecretKey)), //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 ValidateLifetime = true, }; }); #endregion
配置中间件
//启用身份验证功能。必须要在app.UseAuthorization();之前 app.UseAuthentication();//鉴权,
JWTHelper
public class JWTHelper { public class JwtHelper { /// <summary> /// 颁发JWT字符串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJwt(TokenModelJwt tokenModel) { // 自己封装的 appsettign.json 操作类,看下文 string iss = Appsettings.app(new string[] { "Audience", "Issuer" }); string aud = Appsettings.app(new string[] { "Audience", "Audience" }); string secret = Appsettings.app(new string[] { "Audience", "Secret" }); var claims = new List<Claim> { /* * 特别重要: 1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了! 2、你也可以研究下 HttpContext.User.Claims ,具体的你可以看看 Policys/PermissionHandler.cs 类中是如何使用的。 */ new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()), new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //这个就是过期时间,目前是过期7200秒,可自定义,注意JWT有自己的缓冲过期时间 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(7200)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Iss,iss), new Claim(JwtRegisteredClaimNames.Aud,aud), //new Claim(ClaimTypes.Role,tokenModel.Role),//为了解决一个用户多个角色(比如:Admin,System),用下边的方法 }; // 可以将一个用户的多个角色全部赋予; // 作者:DX 提供技术支持; claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: iss, claims: claims, signingCredentials: creds //,expires:DateTime.Now.AddMinutes(1) ); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// <summary> /// 解析 /// </summary> /// <param name="jwtStr"></param> /// <returns></returns> public static TokenModelJwt SerializeJwt(string jwtStr) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); object role; try { jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role); } catch (Exception e) { Console.WriteLine(e); throw; } var tm = new TokenModelJwt { Uid = (jwtToken.Id).ObjToInt(), Role = role != null ? role.ObjToString() : "", }; return tm; } } /// <summary> /// 令牌 /// </summary> public class TokenModelJwt { /// <summary> /// Id /// </summary> public long Uid { get; set; } /// <summary> /// 角色 /// </summary> public string Role { get; set; } /// <summary> /// 职能 /// </summary> public string Work { get; set; } } }
然后呢 如果你使用的Swagger
加上自动在Header带上Token
Bearer就是在ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下实现了一系列认证, 包含 Cookie, JwtBearer, OAuth, OpenIdConnect 等
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间有一个空格)", Name = "Authorization",//jwt默认的参数名称, In = ParameterLocation.Header,//jwt默认存放Autorization信息的位置(header中) Type = SecuritySchemeType.ApiKey, //指定ApiKey BearerFormat = "JWT",//标识承载令牌的格式 该信息主要是出于文档目的 Scheme = "bearer"//授权中要使用的HTTP授权方案的名称 }); //在Heder中添加Token 传递到后台 options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme{ Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer"} },new string[] { } } });
在对应Controller上或Action上加上Authorize标记
验证TOken
options.Events = new JwtBearerEvents { //此处为权限验证失败后触发的事件 OnChallenge = context => { //此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须 context.HandleResponse(); //自定义自己想要返回的数据结果,我这里要返回的是Json对象,通过引用Newtonsoft.Json库进行转换 var payload = JsonConvert.SerializeObject(new ApiResult<bool>("Token无效")); //自定义返回的数据类型 context.Response.ContentType = "application/json"; //自定义返回状态码,默认为401 我这里改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //context.Response.StatusCode = StatusCodes.Status401Unauthorized; //输出Json数据结果 context.Response.WriteAsync(payload); return Task.FromResult(0); } };
/// <summary> /// 获取Token /// </summary> /// <param name="req">请求流</param> /// <returns></returns> public static string GetToken(HttpRequest req) { string tokenHeader = req.Headers["Authorization"].ToString(); if (string.IsNullOrEmpty(tokenHeader)) throw new Exception("缺少token!"); string pattern = "^Bearer (.*?)$"; if (!Regex.IsMatch(tokenHeader, pattern)) throw new Exception("token格式不对!格式为:Bearer {token}"); string token = Regex.Match(tokenHeader, pattern).Groups[1]?.ToString(); if (string.IsNullOrEmpty(token)) throw new Exception("token不能为空!"); return token; }
好了 基本没问题 最后在如果请求401可以自定义
整的代码如下:
[AllowAnonymous] [HttpGet] [Route("api/auth")] public IActionResult Get(string userName, string pwd) { if (CheckAccount(userName, pwd, out string role)) { //每次登陆动态刷新 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(30)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.NameIdentifier, userName), new Claim("Role", role) }; //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, //过期时间 expires: DateTime.Now.AddMinutes(30), //签名证书 signingCredentials: creds, //自定义参数 claims: claims ); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } }
3.3 然后改造一下StartUp.cs
我们仅仅需要关心改动的地方,也就是AddJwtBearer这个验证token的方法,我们不用原先的固定值的校验方式,而提供一个代理方法进行运行时执行校验
.AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = true,//是否验证失效时间 ClockSkew = TimeSpan.FromSeconds(30), ValidateAudience = true,//是否验证Audience //ValidAudience = Const.GetValidudience(),//Audience //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了 AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//是否验证Issuer ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致 ValidateIssuerSigningKey = true,//是否验证SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey };
这里逻辑是这样的:因为重新登陆将原来的变量更改了,所以这里校验的时候也一并修改成了新的变量值,那么旧的token当然就不匹配了,也就是旧的token被强制失效了。
参考地址https://www.cnblogs.com/7tiny/p/11012035.html https://www.cnblogs.com/cokeking/p/10969579.html