【WCF】通过AOP实现基于JWT的授权与鉴权的实践

发布于:2025-07-01 ⋅ 阅读:(23) ⋅ 点赞:(0)

系列文章目录

链接: 【WCF】基于WCF在WinForms搭建RESTful服务指南

链接: 【WCF】单例模式的线程安全缓存管理器实现,给你的WebApi加入缓存吧

链接: 【WCF】基于固定时间窗口的接口限流实现(借助IOperationInvoker的AOP方案)

链接: 【WCF】通过AOP实现基于JWT的授权与鉴权的实践



写在前面

目前流行的前后端这种现代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的验证流程

  1. 服务器生成JWT令牌【令牌颁发服务器】
    一般是在完成登录成功后,服务器从验证通过后,构建Header和Payload。令牌颁发服务器使用保存的密钥对JWT头部和载荷进行签名,生成完整 JWT,并作为请求结果将JWT返回。
  2. 客户端获取并在请求时携带JWT Token
    在上一部中,客户端完成登录成功获取JWT。之后的每次请求都需要在Authorization头部携带该Token,Authorization: Bearer [JWT Token]
  3. 服务器验证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实现鉴权

  1. 自定义特性,标记需要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) { }
}
  1. 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);
        }
        // 提取Token
        string token = authHeader.Substring("Bearer ".Length).Trim();
        // 验证JWT Token
        var 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里实现起来要麻烦的多,但是也不是无法实现的。


网站公告

今日签到

点亮在社区的每一天
去签到