asp.net core 集成JWT(二)token的强制失效,基于策略模式细化api权限

【前言】

  上一篇咱们介绍了什么是JWT,以及如何在asp.net core api项目中集成JWT权限认证。传送门:http://www.javashuo.com/article/p-udxvpans-bq.htmlhtml

  不少博友在留言中提出了疑问:前端

  1. 如何结合jwt认证对用户进行API受权?
  2. token过时了怎么办?
  3. 如何自动刷新token?
  4. 如何强制token失效?
  5. 如何应用到集群模式?

  那么,便有了本篇。本篇在上一篇的基础上继续完善JWT的使用,并陆续回答上面的疑问。固然Demo中没有体现的也会提供思路供博友参考。git

【1、如何结合JWT认证对用户进行API受权】

  场景:咱们有多个API接口,咱们但愿细化地控制哪一个用户能够访问哪些API(多是在某个受权界面进行API受权)github

  仍是咱们上一篇中的Demo项目:https://github.com/sevenTiny/Demo.Jwtredis

  

  咱们添加了两个类:PolicyHandler.cs和PolicyRequirement.cs数据库

  首先是:PolicyRequirement.cs,这个类文件中定义了一个用户名和url的对应实体,UserPermission用户权限承载实体。而后实现了微软自带的接口IAuthorizationRequirement,里面构造方法赋值了若是没有权限将要跳转的接口和某用户全部有权限的接口的配置集合,由于只写了一个接口,这里只配置了一条做为Demo,固然了,在实际应用的时候,全部的这些配置咱们均可以写在数据库中持久化,须要的时候读取出来便可。后端

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;

namespace Demo.Jwt.AuthManagement
{
    /// <summary>
    /// 权限承载实体
    /// </summary>
    public class PolicyRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 用户权限集合
        /// </summary>
        public List<UserPermission> UserPermissions { get; private set; }
        /// <summary>
        /// 无权限action
        /// </summary>
        public string DeniedAction { get; set; }
        /// <summary>
        /// 构造
        /// </summary>
        public PolicyRequirement()
        {
            //没有权限则跳转到这个路由
            DeniedAction = new PathString("/api/nopermission");
            //用户有权限访问的路由配置,固然能够从数据库获取
            UserPermissions = new List<UserPermission> {
                              new UserPermission {  Url="/api/value3", UserName="admin"},
                          };
        }
    }

    /// <summary>
    /// 用户权限承载实体
    /// </summary>
    public class UserPermission
    {
        /// <summary>
        /// 用户名
        /// </summary>
        public string UserName { get; set; }
        /// <summary>
        /// 请求Url
        /// </summary>
        public string Url { get; set; }
    }
}

  PolicyHandler 这个类继承了微软提供的类型AuthorizationHandler<PolicyRequirement>,泛型是咱们上一步刚定义的类型。api

  在这个类里面,咱们实现了抽象方法 Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement),这个方法里面明确了如何具体地校验用户是否有API权限,而且根据校验结果控制应该跳转到提示API,仍是继续执行有权限的API。app

  这里的校验逻辑比较简单,Demo级别的,可是提供了校验的入口,具体业务场景根据需求进行适当替换便可。asp.net

using Microsoft.AspNetCore.Authorization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Demo.Jwt.AuthManagement
{
    public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
        {
            //赋值用户权限
            var userPermissions = requirement.UserPermissions;
            //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息
            var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext;
            //请求Url
            var questUrl = httpContext.Request.Path.Value.ToUpperInvariant();
            //是否通过验证
            var isAuthenticated = httpContext.User.Identity.IsAuthenticated;
            if (isAuthenticated)
            {
                if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl))
                {
                    //用户名
                    var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;
                    if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl))
                    {
                        context.Succeed(requirement);
                    }
                    else
                    {
                        //无权限跳转到拒绝页面
                        httpContext.Response.Redirect(requirement.DeniedAction);
                    }
                }
                else
                {
                    context.Succeed(requirement);
                }
            }
            return Task.CompletedTask;
        }
    }
}

  而后咱们改造一下模拟数据的API,添加一个 api/value3 不一样的是,这个action咱们添加了一个带有策略名称的权限特性标签:[Authorize("Permission")] 经过这个特性标签制定了这个action 会走咱们自定义的策略方法。咱们在返回值里面提示了“这个接口只有管理员才能访问到”,而且返回了登录用户的用户名和角色信息。

[HttpGet]
[Route("api/value3")]
[Authorize("Permission")]
public ActionResult<IEnumerable<string>> Get3()
{
    //这是获取自定义参数的方法
    var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;
    var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
    var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value;
    return new string[] { "这个接口有管理员权限才能够访问", $"userName={userName}",$"Role={role}" };
}

  上文中获取token的方法咱们也微微进行了调整,对不一样的登录用户返回不一样的角色名,让演示更加直观一些,由于改动较小,这里不粘贴代码,有想看详情的请下载代码查看。

  而后咱们改造一下Startup,主要改造的地方是添加了策略模式的配置

services.AddAuthorization(options =>
{
    options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
})

  还有添加了策略模式控制类的依赖注入

//注入受权Handler
services.AddSingleton<IAuthorizationHandler, PolicyHandler>();

  下面是完整的Startup.cs代码

using Demo.Jwt.AuthManagement;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Demo.Jwt
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //添加策略鉴权模式
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
            })
            .AddAuthentication(s =>
            {
                //添加JWT Scheme
                s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            //添加jwt验证:
            .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
                };
                options.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        //Token expired
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            context.Response.Headers.Add("Token-Expired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };
            });

            //注入受权Handler
            services.AddSingleton<IAuthorizationHandler, PolicyHandler>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            ///添加jwt验证
            app.UseAuthentication();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

  咱们完成了这些工做之后,咱们明确咱们的目标:

  1. api/value1 接口咱们不登录就能够直接进行访问
  2. api/value2 接口只有登录用户能够访问,不登陆的用户是没有权限的
  3. api/value3 接口只有admin帐号登录(代码里写死的帐号admin,也只为admin配置了权限)才能够访问,普通用户是不能访问的

  明确了上面的几个目标后,下面咱们进行测试,依然是运行起来咱们的项目:

  1.api/value1 接口咱们不登录就能够直接进行访问

  

  

  咱们没有登录即可以访问到api/value1接口

  2.api/value2 接口只有登录用户能够访问,不登陆的用户是没有权限的

  2.1. 咱们先直接访问api/value2接口

  

  

  返回了状态码:401 无权限

  2.2. 那么咱们调用登录接口获取token

  

  

  2.3. 成功返回了token,咱们拿该token去访问 api/value2 接口

  

  

  能够看到,咱们成功拿到了数据,足以证实,api/value2 接口是须要登录权限的

  3. 那么,咱们用这个token去访问 api/value3 又会怎样呢?

  

  

  返回了403,访问错误。这个403是怎么来的呢?

  咱们上文说过的PolicyHandler.cs文件中若是校验接口没有权限呢,咱们会走下面这段逻辑:

//无权限跳转到拒绝页面
httpContext.Response.Redirect(requirement.DeniedAction);

  

  requirement.DeniedAction是咱们PolicyRequirement.cs文件中配置死的地址:"/api/nopermission"

  

  这个地址返回的就是403 Forbid,固然这里能够根据须要修改返回内容,再也不赘述。

  4. 咱们换一个admin帐号从新登录,而后访问 api/value3 接口

  4.1 首先咱们调用获取token接口进行token获取

  

  

  4.2 咱们拿到一个新的token,而后用这个新的token去访问刚才没权限的接口

  

  

  成功地获取到告终果,说明咱们的配置策略生效了,只有admin帐号才有权限获取到这个接口。

  上面就是咱们完整的策略模式的实现方案,完整的代码能够在github地址中进行下载或clone。

【2、Token的使用策略】

  1.token过时了怎么办?

  关于token过时这个话题呢,有不少应用场景,对应不一样的处理方式。

  好比:token过时能够提示用户从新登录,常见的有登录一段时间后要从新登录校验密码;

  好比:token过时可使用其余手段进行“偷偷”刷新,用户感受不到,可是token已是新的了;

  2.如何自动刷新token

  那么token偷偷刷新有什么实现方式呢?

  好比:约定好失效的时间,前端在失效前进行从新调用登录接口进行获取;

  好比:使用SignalR,保持先后端通信也能够必定时间轮询刷新token;

  好比:后端执行策略,定时任务刷新token,若是持续请求接口,就能够拿到最新的token进行“续命”,若是长时间不访问任意接口,那么token也就失效了;

  3.如何强制token失效?

  什么场景要强制token失效呢?好比咱们只容许帐号一个地方登录一次,异地登录会将帐号挤下线。这种时候咱们就要将旧token失效,仅仅让新的token生效。

  下面咱们在Demo中体现如何让旧token强制失效。

  3.1  在咱们以前说过的Const.cs类中添加一个静态变量(不是const,const是只读的),让咱们在程序中能够直接修改值。固然又是为了模拟,真实场景这个值应该持久化或者存在redis里面,这里咱们为了代码简洁易懂就不集成太多的组件了。

  3.2 稍微修改一下咱们的获取token的action,在密码验证成功以后,修改静态变量的值。

  变量值采用帐号密码加当前时间字符串,以保证每次登录都是不同的值。

//每次登录动态刷新
Const.ValidAudience = userName + pwd + DateTime.Now.ToString();

  而后咱们在生成token的时候,让接收者=咱们静态变量的值,audience: Const.ValidAudience

  完整的代码以下:

[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被强制失效了。

   3.4 咱们实际验证一下

  3.4.1 首先咱们用admin帐号获取token

  

  

  3.4.2 而后用该token访问有权限的 api/value3 接口

  

  

  意料之中,咱们成功访问到了值,并且在有效期内访问屡次都是能够访问成功的。

  3.4.3 那么咱们用admin帐号从新获取token

  

  

  拿到一个新的token

  3.4.4 咱们不更换token,再用旧的token调用一下 api/value3

  

  

  返回状态码401了,说明没有权限了

  

  同时headers里面有错误描述时接收人参数错误,说明一切尽在咱们的预期之中。

  3.4.5 那么咱们使用咱们第二次登录用的新的token进行访问api/value3

  

  

  又成功地获取到了数据,代表咱们新的token占有了当前宝座,老国王已经被挤下台了!

  4. 如何应用到集群模式

  这个问题其实在测试过Demo,而后再结合咱们平常应用的话,答案很容易获得。如下几种参考:

  1. 咱们这个Demo其实相关参数都是从Const.cs常量文件中获取的,文中也说了,实际应用中应从数据库或redis中获取。这些信号都代表了实际应用中不少都是走的配置中心或者是数据库,这些中间件本就自然支持集群模式,所以部署多套服务和部署一套服务是同样的,一个接口能经过的验证,多个接口也一样能经过验证。
  2. 第二种场景在大项目中或者微服务场景中比较常见,那就是微服务网关,咱们彻底能够将JWT集成在微服务网关上,而不用关心具体的下游服务。只要网关能经过认证就能够访问到下游的服务节点。

【结尾】

  到这里,咱们在上一篇中“JWT的简介以及asp.net core 集成JWT”中遗留的问题已经所有解释完毕了,固然了,若是有新的问题也很是欢迎各路朋友在评论区留下您宝贵的意见。

  上一篇传送门:http://www.javashuo.com/article/p-udxvpans-bq.html

  若是想要完整项目源码的,能够参考地址:https://github.com/sevenTiny/Demo.Jwt

  若是有幸能帮助到你,高抬贵手点个star吧~

相关文章
相关标签/搜索