在 aspnet core 中, 自定义 jwt 管道验证
有了上一节的内容作为基础, 那这点也是非常容易的, 关键点在中间件, 只是把上一级在测试类中的自定义验证放到中间件中来即可,
不过需要注意: 中间件 的位置很重要, 只有它后面的管道才会收到影响;
那我们先建一个自定义中间件类:(中间件的详细内容这里就不讲了, 大家可以参考官网和其他博文)
- /// <summary>
- /// 自定义授权中间件
- /// </summary>
- public class JwtCustomerAuthorizeMiddleware
- {
- private readonly RequestDelegate next;
- public JwtCustomerAuthorizeMiddleware(RequestDelegate next, string secret, List<string> anonymousPathList)
- {
- #region 设置自定义 jwt 的秘钥
- if(!string.IsNullOrEmpty(secret))
- {
- TokenContext.securityKey = secret;
- }
- #endregion
- this.next = next;
- UserContext.AllowAnonymousPathList.AddRange(anonymousPathList);
- }
- public async Task Invoke(HttpContext context, UserContext userContext,IOptions<JwtOption> optionContainer)
- {
- if (userContext.IsAllowAnonymous(context.Request.Path))
- {
- await next(context);
- return;
- }
- var option = optionContainer.Value;
- #region 身份验证, 并设置用户 Ruser 值
- var result = context.Request.Headers.TryGetValue("Authorization", out StringValues authStr);
- if (!result || string.IsNullOrEmpty(authStr.ToString()))
- {
- throw new UnauthorizedAccessException("未授权");
- }
- result = TokenContext.Validate(authStr.ToString().Substring("Bearer".Length).Trim(), payLoad =>
- {
- var success = true;
- // 可以添加一些自定义验证, 用法参照测试用例
- // 验证是否包含 aud 并等于 roberAudience
- success = success && payLoad["aud"]?.ToString() == option.Audience;
- if (success)
- {
- // 设置 Ruse 值, 把 user 信息放在 payLoad 中,(在获取 jwt 的时候把当前用户存放在 payLoad 的 ruser 键中)
- // 如果用户信息比较多, 建议放在缓存中, payLoad 中存放缓存的 Key 值
- userContext.TryInit(payLoad["ruser"]?.ToString());
- }
- return success;
- });
- if (!result)
- {
- throw new UnauthorizedAccessException("未授权");
- }
- #endregion
- #region 权限验证
- if (!userContext.Authorize(context.Request.Path))
- {
- throw new UnauthorizedAccessException("未授权");
- }
- #endregion
- await next(context);
- }
- }
上面这个中间件中有个 UserContext 上线文, 这个类主要管理当前用户信息和权限, 其他信息暂时不管; 我们先看一下这个中间件的验证流程如何:
该中间件主要是针对访问的路径进行验证, 当然你也可以针对其他信息进行验证, 比如(控制器名称, 动作名称, 等)
检查当前 url 是否可以匿名访问, 如果可以就直接通过, 不做验证了; 如果不是可以匿名访问的路径, 那就继续
获取当前 http 头部携带的 jwt(存放在头部的 Authorization 中);
使用上一节的讲的 TokenContext 做必须的验证和自定义复杂验证;
获取当前访问用户信息, 我们把用户的基本信息放在 payLoad["ruser"]中, 请看代码如何操作
到这里为止, 都是做的身份验证, 表明你是一个有身份的的人; 接下来是做权限验证, 你是一个有身份的人, 并不代表你是一个随便到处访问的人; 你能访问哪些 url 或者 action, 就要得到权限验证的认可
我们把权限验证放到 userContext.Authorize 方法中(这里怎么操作, 这里就不深入讲解, 基本原理是从数据库或者缓存中获取当前用户对应的权限列表, 也就是 url 列表, 进行对比);
自定义中间件使用 jwt 验证就这些内容, 是不是感觉很清晰, 很简单, 有木有;
中间已经完成了, 那接下来我们来使用它, 我们再 startup 中的 Configure 方法中添加如下代码
App.UseMiddleware<JwtCustomerAuthorizeMiddleware>(Configuration["JwtOption:SecurityKey"], new List<string>() { "/api/values/getjwt","/" });
当然上面可匿名访问的 url 也可以定义在 appsetting.JSON 文件中, 可以自行尝试
如何通过自定义策略形式实现自定义 jwt 验证
创建自定义策略的详细介绍可以参考官网, 这里就不详细介绍,
首先我们上代码, 创建自定义策略非常重要的两个类, 如下:
- public class CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize>
- {
- /// <summary>
- /// 常用自定义验证策略, 模仿自定义中间件 JwtCustomerauthorizeMiddleware 的验证范围
- /// </summary>
- /// <param name="context"></param>
- /// <param name="requirement"></param>
- /// <returns></returns>
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CommonAuthorize requirement)
- {
- var httpContext = (context.Resource as AuthorizationFilterContext).HttpContext;
- var userContext = httpContext.RequestServices.GetService(typeof(UserContext)) as UserContext;
- var jwtOption = (httpContext.RequestServices.GetService(typeof(IOptions<JwtOption>)) as IOptions<JwtOption>).Value;
- #region 身份验证, 并设置用户 Ruser 值
- var result = httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authStr);
- if (!result || string.IsNullOrEmpty(authStr.ToString()))
- {
- return Task.CompletedTask;
- }
- result = TokenContext.Validate(authStr.ToString().Substring("Bearer".Length).Trim(), payLoad =>
- {
- var success = true;
- // 可以添加一些自定义验证, 用法参照测试用例
- // 验证是否包含 aud 并等于 roberAudience
- success = success && payLoad["aud"]?.ToString() == jwtOption.Audience;
- if (success)
- {
- // 设置 Ruse 值, 把 user 信息放在 payLoad 中,(在获取 jwt 的时候把当前用户存放在 payLoad 的 ruser 键中)
- // 如果用户信息比较多, 建议放在缓存中, payLoad 中存放缓存的 Key 值
- userContext.TryInit(payLoad["ruser"]?.ToString());
- }
- return success;
- });
- if (!result)
- {
- return Task.CompletedTask;
- }
- #endregion
- #region 权限验证
- if (!userContext.Authorize(httpContext.Request.Path))
- {
- return Task.CompletedTask;
- }
- #endregion
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
- }
- public class CommonAuthorize: IAuthorizationRequirement
- {
- }
其中两个重要的类是哪两个呢? 他们的作用又是什么呢?
1,CommonAuthorize: IAuthorizationRequirement, 至于取什么名字, 自己定义, 但必须继承 IAuthorizationRequirement, 这类主要是承载一些初始化值, 让后传递到 Handler 中去, 给验证做逻辑运算提供一些可靠的信息; 我这里是空的; 自己根据自身情况自己定义适当的属性作为初始数据的承载容器;
2,CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize > 这个是重点, 承载了验证的逻辑运算
需要重写 override Task HandleRequirementAsync 方法, 所有的逻辑都在该方法中, 他的主要逻辑和上面的自定义中间件很相似, 只少了上面的第一步; 验证流程如下:
获取当前 http 头部携带的 jwt(存放在头部的 Authorization 中);
使用上一节的讲的 TokenContext 做必须的验证和自定义复杂验证;
获取当前访问用户信息, 我们把用户的基本信息放在 payLoad["ruser" 中, 请看代码如何操作
到这里为止, 都是做的身份验证, 表明你是一个有身份的的人; 接下来是做权限验证, 你是一个有身份的人, 并不代表你是一个随便到处访问的人; 你能访问哪些 url 或者 action, 就要得到权限验证的认可
我们把权限验证放到 userContext.Authorize 方法中(这里怎么操作, 这里就不深入讲解, 基本原理是从数据库或者缓存中获取当前用户对应的权限列表, 也就是 url 列表, 进行对比);
context.Succeed(requirement); 是验证成功, 如果没有这个, 就默认验证失败
因为 UserContext 把负责了权限验证, 所以不会把流程搞得感觉很乱, 并且可以重用, 至于用那种形式验证也很容易切换
3, 是不是很简单, 和自定义管道验证的的代码几乎一模一样,
如何使用自定义定义策略呢?
1, 在 startup 类中的 ConfigureServices 中加入如下代码:
- services.AddAuthorization(option =>
- {
- #region 自定义验证策略
- option.AddPolicy("common", policy => policy.Requirements.Add(new CommonAuthorize()));
- #endregion
- }).AddAuthentication(option =>
- {
- option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
- }).AddJwtBearer(option =>
- {
- if (!string.IsNullOrEmpty(config["JwtOption:SecurityKey"]))
- {
- TokenContext.securityKey = config["JwtOption:SecurityKey"];
- }
- // 设置需要验证的项目
- option.TokenValidationParameters = new TokenValidationParameters
- {
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenContext.securityKey))// 拿到 SecurityKey
- };
- });
- // 自定义策略 IoC 添加
- services.AddSingleton<IAuthorizationHandler, CommonAuthorizeHandler>();
以上代码主要分 3 个部分
1, 添加上面自定义的策略, 并取名;
2, 设置秘钥, 这个秘钥就是上一节中生成 jwt 的秘钥, 必须要要一样, 否则是签名不正确
3, 注入上面建立的一个重要类 CommonAuthorizeHandler, 如上面代码
2, 在 startup 类中的 Configure 中添加 App.UseAuthentication();
3, 在需要验证的 Controller 或者 Action 中加上 [Authorize(Policy = "common")] 属性, 看下图:
到此为止你就可以使用自定义策略的验证了;
使用管道和自定义策略两种形式进行验证有什么区别呢?
从效果上看都是一样的, 稍微有点区别
使用管道的方式, 感觉方便点, 清晰点
使用自定义策略的方式, 效率稍微高一点, 毕竟不是所有的请求都会进行是否可以匿名访问运算和建立管道的消耗, 只有加入 Authorize 属性的 Controller 和 Action 的才会进入; 当然这点损耗可以忽略不计, 看自己的喜好;
至于你喜欢那种, 就使用哪种吧, 性能可以忽略不计;
不管使用哪种方式使用 jwt 作为身份和权限验证是不是很简单, 关键这里也把权限验证的逻辑抽出来了, 这样代码就更清晰明了了;
至于 Authorize 的属性形式, 还有很多其他的策略, 比如用户, 申明, 角色等, 可查看官网
来源: https://www.jb51.net/article/149968.htm