【WCF】通过AOP实现基于JWT的授权与鉴权的实践
系列文章目录
链接: 【WCF】基于WCF在WinForms搭建RESTful服务指南
链接: 【WCF】单例模式的线程安全缓存管理器实现,给你的WebApi加入缓存吧
链接: 【WCF】基于固定时间窗口的接口限流实现(借助IOperationInvoker的AOP方案)
链接: 【WCF】通过AOP实现基于JWT的授权与鉴权的实践
文章目录
- 系列文章目录
- 写在前面
- 一、JWT(JSON Web Token)
- 1.1 什么是JWT
- 1.2 JWT的验证流程
- 1.3 JWT令牌的无缝刷新
- 二、AOP思想
- 2.1 AOP思想概述
- 2.2 本文AOP的实现思路
- 三、通过AOP实现基于JWT的授权与鉴权
- 3.1 JWT的颁发与验证
- 3.2 AOP实现鉴权
- 总结
写在前面
目前流行的前后端这种现代Web开发架构模式中,我们常常能听到JWT的授权与鉴权。因为传统项目里基于传统 Cookie-Session验证身份在前后端分离中模式中会碰到:跨域限制(浏览器同源限制),分布式状态(多服务器部署网站服务),状态依赖(Session数据量大)。
本文将通过JWT实现授权。JWT,全称JSON Web Token是一种基于 JSON 的轻量级令牌。服务器无需存储令牌,仅通过签名验证合法性(签名密钥仅服务器持有)。并且因为请求时是通过Authorization头部传递,未使用到Cookie ,也就不存在同源策略对Cookie的严格限制。另外就是该令牌里包含一些基本信息,可用于鉴权,无需每次请求都查基础信息表。这使JWT具备用于在客户端和服务器之间安全传递身份信息和权限声明,广泛应用于分布式系统的授权与鉴权场景。可以说JWT 的诞生顺应了RESTful API和前后端分离架构的兴起。
本文将通过AOP的思想实现鉴权。利用自定义特性,该特性用于标记需要进行Token验证的方法,将 Token 验证逻辑(横切关注点)与业务方法分离作为一个切面。然后在具体的业务方法中使用自定义特性,将Token验证逻辑织入到具体的业务方法中。最后通过一个拦截器,在调用方法前(连接点)执行额外的验证逻辑,达到通知的效果。
本文包含通过AOP实现基于JWT的授权与鉴权这两大内容,将这个现代授权鉴权方式应用到WCF服务中。该尝试起步于对现有业务中的一次安全性升级,其中遇到不少坑点,也有不少个人感悟。在此成文总结,希望能帮助到大家。
提示:以下是本篇文章正文内容,下面案例可供参考
一、JWT(JSON Web Token)
1.1 什么是JWT
JWT全称JSON Web Token, 是一种基于 JSON 的轻量级令牌,用于在网络应用间传递经签名的声明信息(如用户身份、权限等)。其结构分为三部分:
- Header(头部):声明令牌类型(JWT)和签名算法(如 HS256)。
- Payload(载荷):存储核心声明信息(如用户信息、过期时间t等),可自定义字段(如授权角色)。
- Signature(签名):通过头部指定的算法,用密钥(或公钥)对 Header 和 Payload 进行签名,确保令牌未被篡改。
1.2 JWT的验证流程
- 服务器生成JWT令牌【令牌颁发服务器】
一般是在完成登录成功后,服务器从验证通过后,构建Header和Payload。令牌颁发服务器使用保存的密钥对JWT头部和载荷进行签名,生成完整 JWT,并作为请求结果将JWT返回。 - 客户端获取并在请求时携带JWT Token
在上一部中,客户端完成登录成功获取JWT。之后的每次请求都需要在Authorization头部携带该Token,Authorization: Bearer [JWT Token] - 服务器验证JWT令牌
服务器端验证JWT,从请求的Authorization中提取令牌,将其分割为 Header、Payload、Signature 三部分。使用相同的密钥和Header中的签名算法对Header和Payload再次加密对比验证生成的Signature和请求头里解析出来的Signature ,如果不一致则说明存在被篡改的情况。
之后就是验证 Payload里的信息,如验证Token的颁发者,验证Token的接收者,验证Token的过期时间等。
1.3 JWT令牌的无缝刷新
JWT Token常常会遇到令牌过期的情况。一般可采用双令牌的方式实现无缝刷新。主令牌用于接口验证过期时间短,副令牌用于刷新主令牌,过期时间长。服务端访问特定格式的的过期状态给客户端,客户端在副令牌生命周期内都可以成功调用刷新令牌Token的方法,并且执行上一次因主令牌失效导致未执行的请求。从而实现无缝刷新。
还有一种方案是借助于Redis,或者是是借助于缓存。维护一个缓存键值对对象,其中键就是各个用户的唯一标识,值是需要的信息对象(JWT Token,用户信息),并且缓存中该键值对对象的过期要大于JWT令牌的过期时间。在缓存中该键值对对象的生命周期内都可以成功调用刷新令牌Token方法,并且执行上一次因主令牌失效导致未执行的请求。从而实现无缝刷新。
二、AOP思想
2.1 AOP思想概述
AOP全程Aspect-Oriented Programming,中文称之为面向切面编程,它是一种编程范式,旨在将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来,这个AOP的名字直观明了的描述了这种编程范式的实现逻辑。
横切关注点是指那些影响多个模块的功能,例如日志记录、接口限流等。在本文代码示例中,便是接口的鉴权。这些功能在传统的面向对象编程(OOP)中往往会分散在各个模块中,导致代码的耦合度增加,可维护性降低。
AOP 通过将这些横切关注点封装成切面(Aspect),并在特定的连接点(Join Point)上插入这些切面的代码,从而实现了横切关注点与核心业务逻辑的解耦。连接点是程序执行过程中的特定点,例如方法调用、异常抛出等;而切入点(Pointcut)则是用于定义哪些连接点会被切入的表达式。
2.2 本文AOP的实现思路
通过自定义特性实现切入点,通过在方法中使用自定义特性,实现连接点的标记。在自定义特性中,我们通过添加参数检查器,将二者结合起来,组建成一个完整的JWT鉴权切面。最后参数检查器里的BeforeCall方法里通过抛出异常实现通知的功能。
三、通过AOP实现基于JWT的授权与鉴权
3.1 JWT的颁发与验证
JWT的颁发可以作为一个单独的令牌颁发服务器的服务,用于实现分布式架构里多服务消费令牌的场景。所以有了非对称加密和鉴权时验证Issuer的必要。
通过一个静态类工具类,实现生成和验证JWT令牌的静态方法。
通过配置获取JWT设置,并且通过构造函数验证配置
// 从配置获取JWT设置
private static readonly string Secret = ConfigurationManager.AppSettings["Jwt:Secret"];
private static readonly string Issuer = ConfigurationManager.AppSettings["Jwt:Issuer"];
private static readonly string Audience = ConfigurationManager.AppSettings["Jwt:Audience"];
private static readonly int ExpireMinutes = int.Parse(ConfigurationManager.AppSettings["Jwt:ExpireMinutes"]);// 静态构造函数验证配置
static JwtTokenHelper()
{if (string.IsNullOrEmpty(Secret))throw new InvalidOperationException("Jwt:Secret 配置缺失");if (string.IsNullOrEmpty(Issuer))throw new InvalidOperationException("Jwt:Issuer 配置缺失");if (string.IsNullOrEmpty(Audience))throw new InvalidOperationException("Jwt:Audience 配置缺失");if (ExpireMinutes <= 0)throw new InvalidOperationException("Jwt:ExpireMinutes 必须大于0");
}
生成JWT Token
如前文所述,使用密钥和加密方式对Header和Payload加密,其中claims支持权限。
/// <summary>
/// 生成JWT Token
/// </summary>
/// <param name="username"></param>
/// <param name="roles"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static string GenerateToken(string username, string[] roles)
{var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret));var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);var claims = new List<Claim>{new Claim(ClaimTypes.Name, username),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),};claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));var token = new JwtSecurityToken(issuer: Issuer,audience: Audience,claims: claims,expires: DateTime.UtcNow.AddMinutes(ExpireMinutes),signingCredentials: credentials);return new JwtSecurityTokenHandler().WriteToken(token);
}
验证JWT Token
我们发现核心都依赖于JwtSecurityTokenHandler的WriteToken和ValidateToken方法
/// <summary>
/// 验证JWT Token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static ClaimsPrincipal ValidateToken(string token)
{if (string.IsNullOrEmpty(token))return null;// 简单格式检查if (!token.Contains('.') || token.Split('.').Length != 3)return null;var tokenHandler = new JwtSecurityTokenHandler();var key = Encoding.UTF8.GetBytes(Secret);try{var validationParameters = new TokenValidationParameters{ValidateIssuerSigningKey = true,IssuerSigningKey = new SymmetricSecurityKey(key),ValidateIssuer = true,ValidIssuer = Issuer,ValidateAudience = true,ValidAudience = Audience,ValidateLifetime = true,ClockSkew = TimeSpan.Zero};return tokenHandler.ValidateToken(token, validationParameters, out _);}catch{return null;}
}
3.2 AOP实现鉴权
- 自定义特性,标记需要Token验证的方法
该特性里有一个RequiredRoles 属性,方便创建自定义属性的时候注入访问接口需要的权限。
/// <summary>
/// 自定义特性:标记需要Token验证的方法
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ValidateTokenAttribute : Attribute, IOperationBehavior
{/// <summary>/// Attribute里指定的权限/// </summary>public string[] RequiredRoles { get; }public ValidateTokenAttribute(params string[] requiredRoles){RequiredRoles = requiredRoles;}public void ApplyDispatchBehavior(OperationDescription operation, DispatchOperation dispatch){// 添加参数检查器dispatch.ParameterInspectors.Add(new TokenValidationInspector(RequiredRoles));}// 其他接口方法留空public void Validate(OperationDescription operation) { }public void ApplyClientBehavior(OperationDescription operation, ClientOperation clientOperation) { }public void AddBindingParameters(OperationDescription operation, BindingParameterCollection bindingParameters) { }
}
- JWT Token的验证参数检查器
通过抛出异常实现通知的效果。其中_requiredRoles 为自定义特性传过来的接口权限。这里需要手动获取所有角色声明,并且获取角色声明权限的值,用于和通过自定义特性传过来的接口权限进行比较。达到实现鉴权的效果。其余的鉴权内容同理。
/// <summary>
/// JWT Token的验证参数检查器
/// </summary>
public class TokenValidationInspector : IParameterInspector
{private readonly string[] _requiredRoles;public TokenValidationInspector(params string[] requiredRoles){_requiredRoles = requiredRoles;}public object BeforeCall(string operationName, object[] inputs){WebOperationContext context = WebOperationContext.Current;if (context == null){throw new WebFaultException<JsonResult<string>> (new JsonResult<string>(false, "服务有误"),HttpStatusCode.InternalServerError);}// 验证Authorization头var requestHeaders = context.IncomingRequest.Headers;string authHeader = requestHeaders["Authorization"];if (string.IsNullOrEmpty(authHeader)){throw new WebFaultException<JsonResult<string>>(new JsonResult<string>(false, "请求头未指定验证参数"),HttpStatusCode.Unauthorized);}if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)){throw new WebFaultException<JsonResult<string>>(new JsonResult<string>(false, "验证参数不合规,请使用Bearer token格式"),HttpStatusCode.Unauthorized);}// 提取Tokenstring token = authHeader.Substring("Bearer ".Length).Trim();// 验证JWT Tokenvar principal = JwtTokenHelper.ValidateToken(token);if (principal == null){throw new WebFaultException<JsonResult<string>>(new JsonResult<string>(false, "无效或过期的token"),HttpStatusCode.Forbidden);}// 验证权限var claimsPrincipal = principal as ClaimsPrincipal;if (claimsPrincipal != null){//获取所有角色声明var roleClaims = claimsPrincipal.FindAll(ClaimTypes.Role);//获取角色声明权限的值string[] userRoles = roleClaims.Select(c => c.Value.Trim()).ToArray();if (!_requiredRoles.Any(requiredRole => userRoles.Contains(requiredRole.Trim()))){throw new WebFaultException<JsonResult<string>>(new JsonResult<string>(false, "权限不匹配"),HttpStatusCode.Forbidden);}}return null;}public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState){// 不需要实现}
}
总结
本文核心是在WCF服务中结合AOP思想与JWT技术实现授权与鉴权。不得不说,通过WCF实现基于JWT的授权与鉴权比ASP.NET Core WebApi里实现起来要麻烦的多,但是也不是无法实现的。