JWT权限验证,兼容多方式验证
前言
许久没写博文了,整合下这段时间所学吧,前进路上总要停下来回顾下学习成果。
本篇记录下项目的权限验证,WebApi项目中用权限验证来保证接口安全总是需要的,然而权限验证的方式多种多样,博主在项目中使用的多的也就是JWT了,一般都是写完之后万年不动~~
所以,本篇算是对鉴权授权的回顾与总结
JWT
至于什么是JWT(https://jwt.io/),只要不是小白都知道吧,不知道的去看下JWT的结构原理这些,偷偷补下课,JWT(JSON Web Token)名字可看出来这是Json格式的web凭证,也就是一个令牌,只有拿到这个Token才能访问到接口,否则请求接口之后会返回401HTTP状态码,401状态码表示未授权,而想要拿到服务器的Token,必须通过服务器验证,一般这个验证来自登录之后返回出来,如果是开发平台一般是通过AppId和Secrect来获取到Token,获取到Token后将Token添加到请求头中,服务器收到请求后,获取到请求头的Token后一验证,“诶!~是我发布的Token,通过!”,随后才能进入控制器。
引入
先把JWT引入到项目中来,目前最新版本为稳定版5.0.2
nuget : Microsoft.AspNetCore.Authentication.JwtBearer
鉴权授权
首先要知道沃恩需要什么样的鉴权策略,在生成Token时的策略就必须保持一致。在WebApi中我们不知道是谁在访问服务器,当然想要知道还是可以的,这时可以通过Token将用户信息传到服务器,我们知道JWT的负载信息除了已经准备好的"sub"、"name"、"iat"这些信息,我们还能自定义我们需要的字段,比如登录人的UserId,UserName,AppId等……
首先需要一个接口 IAuthService
public interface IAuthService { /// <summary> /// 判断权限 /// </summary> /// <param name="token"></param> /// <param name="path"></param> /// <returns></returns> Task<bool> PermissionAsync(string token, string path); /// <summary> /// 获取用户 /// </summary> /// <param name="token"></param> /// <returns></returns> Task SetUserAsync(string token); }
以后所有的权限类型都可以使用这个接口,先将JWT需要的类包装下,这样就能直接使用了
public static class JwtUtils { /// <summary> /// 生成token /// </summary> /// <param name="claims"></param> /// <returns></returns> public static string CreateToken(IEnumerable<Claim> claims, string securityKey) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512); var securityToken = new JwtSecurityToken( issuer: null, audience: null, claims: claims, //expires: DateTime.Now.AddMinutes(settings.ExpMinutes), signingCredentials: creds); var token = new JwtSecurityTokenHandler().WriteToken(securityToken); return token; } /// <summary> /// 生成Jwt /// </summary> /// <param name="userName"></param> /// <param name="roleName"></param> /// <param name="userId"></param> /// <returns></returns> public static string GenerateToken(string userId, string securityKey) { //声明claim var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Typ,"JWT"), new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(JwtRegisteredClaimNames.Iat,DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),ClaimValueTypes.Integer64), new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMonths(2).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), //过期时间 }; return CreateToken(claims, securityKey); } ///// <summary> ///// 刷新token ///// </summary> ///// <returns></returns> //public static string RefreshToken(string oldToken) //{ // var pl = GetPayload(oldToken); // //声明claim // var claims = new Claim[] { // new Claim(JwtRegisteredClaimNames.Sub, pl?.UserName), // new Claim(JwtRegisteredClaimNames.Jti, pl?.UserId), // new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixDate().ToString(), ClaimValueTypes.Integer64),//签发时间 // new Claim(JwtRegisteredClaimNames.Nbf, DateTime.UtcNow.ToUnixDate().ToString(), ClaimValueTypes.Integer64),//生效时间 // new Claim(JwtRegisteredClaimNames.Exp, DateTime.Now.AddMinutes(settings.ExpMinutes).ToUnixDate().ToString(), ClaimValueTypes.Integer64), //过期时间 // new Claim(JwtRegisteredClaimNames.Iss, settings.Issuer), // new Claim(JwtRegisteredClaimNames.Aud, settings.Audience), // new Claim(ClaimTypes.Name, pl?.UserName), // new Claim(ClaimTypes.Role, pl?.RoleId), // new Claim(ClaimTypes.Sid, pl?.UserId) // }; // return IsExp(oldToken) ? CreateToken(claims) : null; //} /// <summary> /// 从token中获取用户身份 /// </summary> /// <param name="token"></param> /// <returns></returns> public static IEnumerable<Claim> GetClaims(string token) { var handler = new JwtSecurityTokenHandler(); var securityToken = handler.ReadJwtToken(token); return securityToken?.Claims; } /// <summary> /// 从Token中获取用户身份 /// </summary> /// <param name="token"></param> /// <param name="securityKey">securityKey明文,Java加密使用的是Base64</param> /// <returns></returns> public static ClaimsPrincipal GetPrincipal(string token, string securityKey) { try { var handler = new JwtSecurityTokenHandler(); TokenValidationParameters tokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)), ValidateLifetime = false }; return handler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken); } catch (Exception ex) { return null; } } /// <summary> /// 校验Token /// </summary> /// <param name="token">token</param> /// <returns></returns> public static bool CheckToken(string token, string securityKey) { var principal = GetPrincipal(token, securityKey); if (principal is null) { return false; } return true; } /// <summary> /// 获取Token中的载荷数据 /// </summary> /// <param name="token">token</param> /// <returns></returns> public static JwtPayload GetPayload(string token) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken securityToken = jwtHandler.ReadJwtToken(token); return new JwtPayload { sub = securityToken.Payload[JwtRegisteredClaimNames.Sub]?.ToString(), exp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(securityToken.Payload[JwtRegisteredClaimNames.Exp].ToString())).ToLocalTime().DateTime, iat = securityToken.Payload[JwtRegisteredClaimNames.Iat]?.ToString() }; } /// <summary> /// 获取Token中的载荷数据 /// </summary> /// <typeparam name="T">泛型</typeparam> /// <param name="token">token</param> /// <returns></returns> public static T GetPayload<T>(string token) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(token); return JsonConvert.DeserializeObject<T>(jwtToken.Payload.SerializeToJson()); } /// <summary> /// 判断token是否过期 /// </summary> /// <param name="token"></param> /// <returns></returns> public static bool IsExp(string token) { return false; //return GetPrincipal(token)?.Claims.First(c => c.Type == JwtRegisteredClaimNames.Exp)?.Value?.TimeStampToDate() < DateTime.Now; //return GetPayload(token).ExpTime < DateTime.Now; } } /// <summary> /// Jwt载荷信息 /// </summary> public class JwtPayload { public string sub { get; set; } public string iat { get; set; } public DateTime exp { get; set; } }
JWT服务实现
public class AuthSettings { public string Secret { get; set; } public string Issuer { get; set; } public double Expire { get; set; } }
public class AuthServiceImpl : IAuthService { private readonly AuthSettings _authSettings; private readonly LoginUser _currentUser; public AuthServiceImpl(IOptions<AuthSettings> authSettings, LoginUser currentUser) { _authSettings = authSettings.Value; _currentUser = currentUser; } public Task<bool> PermissionAsync(string token) { return Task.FromResult(JwtUntil.CheckToken(token, _authSettings.Secret)); } public Task SetUserAsync(string token) { var payload = JwtUntil.GetPayload(token); _currentUser.UserId = payload.UserId; _currentUser.RoleType = payload.Role; return Task.CompletedTask; } }
说到这还没有注册JWT,我们先注册到项目中,验证策略自己定,记得要先注入下服务
services.AddScoped<IAuthService, AuthServiceImpl>(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { x.RequireHttpsMetadata = false;//元数据地址或权限是否需要https x.SaveToken = true;//是否将存储信息保存在token中 x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateLifetime = true,//是否验证过期时间 LifetimeValidator = (notBefore, expire, securityToken, validationparameters) => { bool t = DateTime.UtcNow < expire; return t; }, ValidateAudience = false,//是否验证被发布者 ValidateIssuer = true,//是否验证发布者 ValidIssuer = configuration["AuthSettings:Issuer"], ValidateIssuerSigningKey = true,//是否验证签名 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["AuthSettings:Secret"])) }; });
#region 授权鉴权 /授权 app.UseAuthentication(); //鉴权 app.UseAuthorization(); #endregion
接下来就是设置当前登录用户,获取当前登录用户:发布时在 claims中加入自定义的UserId等信息,授权时获取当前Token解析claims中用户信息属性即可获取到当前请求用户。
本项目中使用的是一个中间件
public sealed class LoginMiddlerware { private readonly RequestDelegate _next; public LoginMiddlerware(RequestDelegate next) { _next = next; } /// <summary> /// 设置登录用户 /// </summary> /// <param name="context"></param> /// <param name="_authService"></param> /// <returns></returns> public async Task InvokeAsync(HttpContext context, IEnumerable<IAuthService> _authService) { string token = GetToken(); if (!string.IsNullOrEmpty(token)) { if (token.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase) || token.Contains('.')) { await _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).SetUserAsync(token); } else if (token.StartsWith("App", StringComparison.OrdinalIgnoreCase)) { await _authService.First(a => a.ServiceName == nameof(AppAuthServiceImpl)).SetUserAsync(token); } else { await _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).SetUserAsync(token); } } await _next.Invoke(context); string GetToken() { string token = context.Request.Headers["token"]; if (string.IsNullOrEmpty(token)) { token = context.Request.Query["token"]; } return token; } } }
到这里JWt就差不多讲完了,接下来是使用,使用时无非就是在控制器或方法上打上标记,如要同时兼容多种授权方式,可以自己写一个Attribute
/// <summary> /// 自定义授权验证特性 /// </summary> public class RequiresPermissionsAttribute : TypeFilterAttribute { public RequiresPermissionsAttribute(ClaimType claimType, string claimValue = "") : base(typeof(ClaimRequirementFilter)) { Arguments = new object[] { new Claim(claimType.ToString(), claimValue) }; } } public class ClaimRequirementFilter : IAuthorizationFilter { readonly Claim _claim; readonly IEnumerable<IAuthService> _authService; private readonly WinkSignSettings _winkSignSettings; public ClaimRequirementFilter(Claim claim, IEnumerable<IAuthService> authService, IOptions<WinkSignSettings> winkSignSettings) { _claim = claim; _authService = authService; _winkSignSettings = winkSignSettings.Value; } public void OnAuthorization(AuthorizationFilterContext context) { ControllerActionDescriptor controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; if (controllerActionDescriptor != null) { var skipAuthorization = controllerActionDescriptor.MethodInfo.GetCustomAttributes(inherit: true) .Any(a => a.GetType().Equals(typeof(AllowAnonymousAttribute))); if (skipAuthorization) { return; } } ClaimType claimType = Enum.Parse<ClaimType>(_claim.Type); bool permission = false; string token = GetToken(); if (string.IsNullOrEmpty(token)) { context.Result = new UnauthorizedResult(); return; } if (claimType == ClaimType.JwtOrOauth2) { //根据Token类型选择认证方式 if (token.Any(t => t == '.')) { claimType = ClaimType.JWT; } else { claimType = ClaimType.Oauth2; } } permission = claimType switch { ClaimType.Oauth2 or ClaimType.Cookie => _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result, ClaimType.JWT => _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result, ClaimType.Key => _authService.First(a => a.ServiceName == nameof(KeyAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result, ClaimType.App => _authService.First(a => a.ServiceName == nameof(AppAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result, }; if (!permission) { context.Result = new UnauthorizedResult(); return; } string GetToken() { string token = context.HttpContext.Request.Headers["token"]; if (string.IsNullOrEmpty(token)) { token = context.HttpContext.Request.Query["token"]; } if (string.IsNullOrEmpty(token)) { context.HttpContext.Request.Cookies.TryGetValue("token", out token); } return token; } } } public enum ClaimType { Oauth2, JWT, Cookie, Key, App, JwtOrOauth2 }
这样就能实现多个方式同时存在,想用哪个就用哪个了,只要实现 IAuthService 接口就行