asp.net core 集成JWT

asp.net core 集成JWT(一)

【什么是JWT】

  JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。

  JWT的官网地址:https://jwt.io/

  通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。

  JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSAECDSA的公钥/私钥对进行签名

【什么时候应该使用JSON Web令牌?】

  1. 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。Single Sign On是一种现在广泛使用JWT的功能,因为它的开销很小,并且能够在不同的域中轻松使用。

  2. 信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。

【JWT有什么优势?】

  我们先看我们传统的身份校验方式

  1. 用户向服务器发送用户名和密码。
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。
  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

  这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。如果session存储的节点挂了,那么整个服务都会瘫痪,体验相当不好,风险也很高。

  相比之下,JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。

【JWT令牌结构】

  在紧凑的形式中,JSON Web Tokens由dot(.分隔的三个部分组成,它们是:

  • Header 头
  • Payload 有效载荷
  • Signature 签名

  因此,JWT通常如下所示:

  xxxxx.yyyyy.zzzzz

  1.Header 头

  标头通常由两部分组成:令牌的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。

  例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

  然后,这个JSON被编码Base64Url,形成JWT的第一部分。

  2.Payload 有效载荷

  Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人

  • exp (expiration time):过期时间

  • sub (subject):主题

  • aud (audience):受众

  • nbf (Not Before):生效时间

  • iat (Issued At):签发时间

  • jti (JWT ID):编号

  除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

  注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

  3.Signature 签名

  Signature 部分是对前两部分的签名,防止数据篡改。

  首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

  签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发件人是否是它所声称的人。  

  把他们三个全部放在一起

  输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。

  下面显示了一个JWT,它具有先前的头和​​有效负载编码,并使用机密签名。 

  

  如果您想使用JWT并将这些概念付诸实践,您可以使用jwt.io Debugger来解码,验证和生成JWT。

   

【JSON Web令牌如何工作?】

  在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。

  每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中标题的内容应如下所示:

  Authorization: Bearer <token>

  在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization标头中的有效JWT ,如果存在,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。

  如果在标Authorization头中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。

  下图显示了如何获取JWT并用于访问API或资源:

  

  1. 应用程序向授权服务器请求授权
  2. 校验用户身份,校验成功,返回token
  3. 应用程序使用访问令牌访问受保护的资源

【ASP.Net Core 集成JWT】

  前面我们介绍了JWT的原理,下面我们在asp.net core实际项目中集成JWT。

  首先我们新建一个Demo asp.net core 空web项目

  

  添加数据访问模拟api,ValuesController

  其中api/value1是可以直接访问的,api/value2添加了权限校验特性标签 [Authorize]

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Demo.Jwt.Controllers
{
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        [Route("api/value1")]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value1" };
        }

        [HttpGet]
        [Route("api/value2")]
        [Authorize]
        public ActionResult<IEnumerable<string>> Get2()
        {
            return new string[] { "value2", "value2" };
        }
    }
}
复制代码

  添加模拟登陆,生成Token的api,AuthController

  这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。

复制代码
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace Demo.Jwt.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        [AllowAnonymous]
        [HttpGet]
        public IActionResult Get(string userName, string pwd)
        {
            if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd))
            {
                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.Name, userName)
                };
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                var token = new JwtSecurityToken(
                    issuer: Const.Domain,
                    audience: Const.Domain,
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(30),
                    signingCredentials: creds);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token)
                });
            }
            else
            {
                return BadRequest(new { message = "username or password is incorrect." });
            }
        }
    }
}
复制代码

  Startup添加JWT验证的相关配置

复制代码
using Microsoft.AspNetCore.Authentication.JwtBearer;
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.Text;


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)
        {
            //添加jwt验证:
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否验证Issuer
                        ValidateAudience = true,//是否验证Audience
                        ValidateLifetime = true,//是否验证失效时间
                        ClockSkew = TimeSpan.FromSeconds(30),
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        ValidAudience = Const.Domain,//Audience
                        ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                    };
                });

            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?}");
            });
        }
    }
}
复制代码

  最后把代码里面用到的一些相关常量也粘贴过来,Const.cs

复制代码
namespace Demo.Jwt
{
    public class Const
    {
        /// <summary>
        /// 这里为了演示,写死一个密钥。实际生产环境可以从配置文件读取,这个是用网上工具随便生成的一个密钥
        /// </summary>
        public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB";
        public const string Domain = "http://localhost:5000";
    }
}
复制代码

  到这里,已经是我们项目的所有代码了。

  如果需要完整的项目代码,Github地址:https://github.com/sevenTiny/Demo.Jwt

【JWT测试】

   我们找一个趁手的工具,比如fiddler,然后把我们的web站点运行起来

  首先调用无权限的接口:http://localhost:5000/api/value1

  

  

  正确地返回了数据,那么接下来我们测试JWT的流程

  1. 无权限

  首先我们什么都不加调用接口:http://localhost:5000/api/value2

  

  

  返回了状态码401,也就是未经授权:访问由于凭据无效被拒绝。 说明JWT校验生效了,我们的接口收到了保护。

  2.获取Token

  调用模拟登陆授权接口:http://localhost:5000/api/Auth?userName=zhangsan&pwd=123

  这里的用户密码是随便写的,因为我们模拟登陆只是校验了下非空,因此写什么都能通过

  

  成功得到了响应

  

  

  然后我们得到了一个xxx.yyy.zzz 格式的 token 值。我们把token复制出来

  3.在刚才401的接口请求HEADER中添加JWT的参数,把我们的token加上去

  再次调用我们的模拟数据接口,但是这次我们加了一个HEADER:http://localhost:5000/api/value2

  

  把内容粘出来

User-Agent: Fiddler
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o

  这里需要注意 Bearer 后面是有一个空格的,然后就是我们上一步获取到的token

  

  

  嗯,没有401了,成功返回了数据

  4.JWT的Token过期

  我们且倒一杯开水,坐等30分钟(我们代码中设置的过期时间),然后再次调用数据接口:http://localhost:5000/api/value2

  

  

  又变成了401,我们看下详细的返回数据

  

  这里有标注,错误描述 token过期,说明我们设置的token过期时间生效了

  5.JWT添加自定义的参数(比如带上用户信息)

  假如我们想在认证通过的时候,直接从jwt的token中获取到登陆的用户名,该怎么操作呢?

  首先在我们的获取token 的api接口里面添加一个Claim节点,key可以随便给,也可以使用已经提供好的一些预置Key,value是我们登陆的userName(仅作为演示)

  

  然后在我们的模拟数据接口获取自定义参数

  

  这里使用HttpContext的授权扩展方法,拿到认证的信息,我们来看下结果

  

  

  请求成功返回,并且也拿到了我们一开始写入的userName

【评论区的一些问题】

  1.token过期了怎么办?

  token过期了说明登陆信息已经过期,需要重新登陆,跳转到登录页重新登陆获取新的token。(当然自动刷新token除外)

  2.如何交换新的token

  如果要保证token长期有效,可以前端在过期前调用登陆接口刷新token。或者使用SignalR轮询,定期刷新token。

  3.如何强制token失效?

  我们有个ValidAudience(接收人),可以利用这个标准参数,登陆时候生成一个GUID,在数据库/Redis/xxx存一份,然后验证接口的时候再把这个值拿出来去一起校验。如果值变了校验就失败了,当然,重新登陆就会刷新这个值,所以只要重新登陆,旧的token也就失效了。

  4.如何应用到集群模式

  当前Demo里面,我们验证jwt的所有参数都是Const常量写死的,但是在真实生产环境都是可以走统一的配置中心,所以集群场景下,一个token可以在多个服务上被验证通过,因为校验token正确的密钥和相关参数都是从配置中心获取的。

【结束】

  到这里,我们JWT的简介以及asp.net core 集成JWT已经完美完成,当然了这只是一个demo,在实际的应用中需要补充和完善的地方还有很多。

  这一篇文章中评论区的一些疑问我放在了下一篇文章逐一解决,有兴趣的朋友请移步下文:asp.net core 集成JWT(二)token的强制失效,基于策略模式细化api权限

  如果想要完整项目源码的,可以参考地址:https://github.com/sevenTiny/Demo.Jwt

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

 

出处:https://www.cnblogs.com/7tiny/p/11012035.html

=======================================================================================

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

【前言】

  上一篇我们介绍了什么是JWT,以及如何在asp.net core api项目中集成JWT权限认证。传送门:https://www.cnblogs.com/7tiny/p/11012035.html

  很多博友在留言中提出了疑问:

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

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

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

  场景:我们有多个API接口,我们希望细化地控制哪个用户可以访问哪些API(可能是在某个授权界面进行API授权)

  还是我们上一篇中的Demo项目:https://github.com/sevenTiny/Demo.Jwt

  

  我们添加了两个类: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>,泛型是我们上一步刚定义的类型。

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

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

复制代码
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。

【二、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”中遗留的问题已经全部解释完毕了,当然了,如果有新的问题也非常欢迎各路朋友在评论区留下您宝贵的意见。

  上一篇传送门:https://www.cnblogs.com/7tiny/p/11012035.html

  如果想要完整项目源码的,可以参考地址:https://github.com/sevenTiny/Demo.Jwt

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

 

出处:https://www.cnblogs.com/7tiny/p/11019698.html

posted on 2021-07-13 10:29  jack_Meng  阅读(774)  评论(0编辑  收藏  举报

导航