ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程

翻译自:地址

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第1张图片

在今年年初,我整理了有关将JWT身份验证与ASP.NET Core Web API和Angular一起使用的详细指南。目前有120多个评论,它是Internet上这个角落中最繁忙的页面,这可能表明许多开发人员在连接身份验证时面临的挑战。

如果我不得不选择该帖子中缺少的一项重要内容,那可能是刷新令牌及其在JWT身份验证和授权工作流程中的微妙而必不可少的角色。我认为将那篇文章中的Web API项目更新和重组为一个包含刷新令牌的独立JWT解决方案是值得的-因此,请为您喜欢的饮料重新装瓶,然后开始吧。 ☕️

JWT复习

现代的身份验证和授权协议使用令牌作为仅携带足够数据以授权用户执行操作或从资源请求数据的方法。简而言之,令牌是允许进行某些授权过程的信息包。 JWT令牌特别提供了一种非常方便的方式,以权利要求的形式打包有关用户的通用属性。关于声明的好处是,它们可以被信任并可以反复验证,因为在大多数情况下,它们是使用带有HMAC算法的私钥进行数字签名的。该签名确保只有拥有密钥的服务器才能解码和验证传入令牌的内容,并授予或拒绝对其资源的访问。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第2张图片

刷新令牌

上图在说明身份验证服务器如何获取访问令牌,然后如何在随后的访问受保护资源的请求中交换访问令牌方面相对简单。但是,如果我们对它进行足够长的研究,我们应该提出一个关键问题:访问令牌的生存期有多长?它会持续一个小时,一天或一个月吗?

这个问题很重要,因为如果某些恶意方要持有令牌,那么他们可以在冒充真实接收者的同时,终身使用令牌。发生这种情况是因为服务器将始终信任带有有效签名的JWT令牌。

在这一点上,使受损令牌无效的唯一方法是修改用于对其签名的密钥-但是,如果这样做,我们将使每个用户的每个已发行令牌无效!为此目的更改密钥是不可接受的方法,而这个确切的问题是打算解决刷新令牌。

刷新令牌仅保存获取新访问令牌所需的信息。它们主要是一次性令牌,可以将其交换为身份验证服务器发行的新访问令牌。主要用例是使用过期的旧访问令牌进行交易。在这种情况下,客户端可以获取新的JWT,而无需重新进行身份验证,因此,无需在每次访问令牌到期时都要求用户输入凭据即可。根据实现方式和生命周期的不同,令牌的有效期为-分钟,小时等。这为用户提供了无缝体验,同时保持了更高的安全性。

更好的是,如果刷新令牌遭到破坏,则可以将其撤销或列入黑名单,因此当任何客户端应用尝试将其交换为新的访问令牌时,该请求将被拒绝,迫使用户重新输入其凭据并通过服务器验证。

令牌生命周期

您的访问令牌和刷新令牌有效的时间长度将在很大程度上取决于您独特的应用程序和安全性要求。通常,访问令牌被认为是短期的,这意味着它们可以在颁发后几分钟到几小时内过期,而刷新令牌则具有较长的寿命和更长的寿命,并被安全地存储以保护它们免受潜在攻击者的侵害。

我们已经介绍了有关刷新令牌在JWT身份验证流程中扮演的角色的理论知识。现在,让我们看一下使用ASP.NET Core Web API,Identity和Entity Framework Core实现它们的方法。

注册用户

当然,任何身份验证系统的主要主题都是用户。我们的项目没有什么不同,因此我们的第一步是在数据层中添加功能,以创建和保留新的用户帐户。对我们来说幸运的是,ASP.NET Core Identity系统通过提供注册用户并将其凭据,配置文件数据等存储在数据库中所需的所有API和集成为我们提供了支持。在本教程中,我们将使用Sql Server Express,但EFCore 支持其他数据库包括Azure表存储,MySql,PostgreSQL等。

Entity Framework Core 和 Identity

首先,我将用于Entity Framework Core和Identity的必需软件包放入基础结构项目中。如果您对正在使用的软件包的确切列表感兴趣,请查看Web.Api.Infrastructure.csproj文件。

接下来,我创建了AppUser类,该类继承自IdentityUser-Identity框架使用的一种内置类型,用于保存有关用户的基本信息,例如电子邮件,用户名,密码等。默认情况下,该类映射到AspNetUsers表,我们将看到它不久。以这种方式对IdentityUser进行子类化为我们提供了一个扩展点,可以向身份模型添加任何自定义属性。我没有在此样本项目中添加任何内容,但认为值得一提。

public class AppUser : IdentityUser
{
    //通过向此类添加属性来为应用程序用户添加其他配置文件数据
}

有了用户模型,此步骤中将添加更多样板代码。首先是AppIdentityDbContext,它只是实体框架核心用于身份的上下文类。我们将很快为主应用程序添加第二个DbContext。接下来,我添加了AppIdentityDbContextFactory,它允许实体框架工具直接从我们的基础结构类库生成迁移。默认情况下,它使用其他约定从我们的实体类型,DbContext等中收集必要的信息以生成迁移,但是我们将绕过这些约定,而改用设计时工厂。

DesignTimeDbContextFactoryBase实现IDesignTimeDbContextFactory接口
用于创建派生的DbContext实例的工厂。实现此接口可为没有公共默认构造函数的上下文类型启用设计时服务。在设计时,可以创建派生的DbContext实例,以启用特定的设计时体验,例如迁移。设计时服务将自动发现该接口的实现,这些实现位于启动程序集或与派生上下文相同的程序集中。

public class AppIdentityDbContextFactory : DesignTimeDbContextFactoryBase
{
  protected override AppIdentityDbContext CreateNewInstance(DbContextOptions  options)
  {  
    return new AppIdentityDbContext(options);
  }
}

我们需要做的下一件事是在ASP.NET Core中间件中连接身份提供程序。我在Startup.cs的ConfigureServices()方法中添加了必要的配置。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
   // Add framework services.
   services.AddDbContext(
     options => options.UseSqlServer(
       Configuration.GetConnectionString("Default"), 
       b => b.MigrationsAssembly("Web.Api.Infrastructure"))
       );
...
   // add identity
   var identityBuilder = services.AddIdentityCore(o =>
   {
     // configure identity options
     o.Password.RequireDigit = false;
     o.Password.RequireLowercase = false;
     o.Password.RequireUppercase = false;
     o.Password.RequireNonAlphanumeric = false;
     o.Password.RequiredLength = 6;
   });
...

处理完之后,我添加了一个带有Create()方法的UserRepository来将新用户存储在数据库中。

public async Task Create(string firstName, string lastName, string email, string userName, string password)
{
   var appUser = new AppUser {Email = email, UserName = userName};
   var identityResult = await _userManager.CreateAsync(appUser, password);

   if (!identityResult.Succeeded) return new CreateUserResponse(appUser.Id, false,identityResult.Errors.Select(e  => new Error(e.Code, e.Description)));

   var user = new User(firstName, lastName, appUser.Id, appUser.UserName);
   _appDbContext.Users.Add(user);

   await _appDbContext.SaveChangesAsync();

   return new CreateUserResponse(appUser.Id, identityResult.Succeeded, identityResult.Succeeded ? null :  identityResult.Errors.Select(e => new Error(e.Code, e.Description)));
}

在这种方法中,我们首先使用提供的_userManager框架将新的用户身份保存在AspNetUsers表中。接下来,我们保存将由关联的域实体使用的其他用户信息。您会注意到这使用了第二个上下文:_appDbContext,它包含一个User实体模型。

使用EF Core迁移创建数据库

定义了初始用户模型后,我们就可以创建将用于为我们生成架构和数据库的迁移。在基础结构项目文件夹中,我运行了以下命令来创建和应用应用程序和身份上下文迁移。请注意,此处使用–context标志来标识目标上下文。因为我们在同一程序集中定义了两个,所以需要此标志来让EF Core工具知道在生成目标迁移时要使用哪个。

Web.Api.Infrastructure>dotnet ef migrations add initial --context AppIdentityDbContext
Web.Api.Infrastructure>dotnet ef migrations add initial --context AppDbContext
Web.Api.Infrastructure>dotnet ef database update --context AppIdentityDbContext
Web.Api.Infrastructure>dotnet ef database update --context AppDbContext

运行这些命令后,我在localdb实例中找到了一个新数据库。

注册用户用例

在数据层到位之后,我进入业务层并编写了RegisterUserUseCase,它基本上只是调用存储库,并将结果通过输出端口传递给我们的API(我们将转到下一个)以用于其响应中。 。概念用例和输出端口的灵感来自The Clean Architecture。如果您想了解有关Clean Architecture的更多信息,请查看我以前的文章或Bob叔叔的精彩介绍。

public async Task Handle(RegisterUserRequest message, IOutputPort outputPort)
{
   var response = await _userRepository.Create(message.FirstName, message.LastName,message.Email, message.UserName, message.Password);
   outputPort.Handle(response.Success ? new RegisterUserResponse(response.Id, true) : new RegisterUserResponse(response.Errors.Select(e => e.Description)));
   return response.Success;
}

Accounts Controller

有了业务和基础架构层之后,我们就可以设置控制器并执行用于注册新用户帐户的操作。我添加了一个带有单个操作方法的新AccountsController,该方法将在包含正文中用户详细信息的http POST请求上触发。该消息包含通过用例和数据层向下传递以执行操作的数据。

// POST api/accounts
[HttpPost]
public async Task Post([FromBody] Models.Request.RegisterUserRequest request)
{
  if (!ModelState.IsValid)
  {
    return BadRequest(ModelState);
  }
  
  await _registerUserUseCase.Handle(new RegisterUserRequest(request.FirstName,request.LastName,request.Email, request.UserName,request.Password), _registerUserPresenter);
  return _registerUserPresenter.ContentResult;
}

松耦合和IoC

为了使项目中的层和组件保持松散耦合,请在Autofac中注册所有内容。基础结构和核心项目在模块中注册了它们各自的服务,这些模块被连接到Web.Api项目中。我们可以使用框架提供的内置依赖项注入容器,但是我们将其替换为Autofac,并使用其模块来捆绑和组织解决方案中各个项目的依赖项。

使用Swagger进行API测试

我们已经准备好运行项目并测试刚刚完成的端点。为了简化测试,我通过nuget添加了Swashbuckle.AspNetCore程序包,然后在Startup.cs中配置了必要的位,从而为Web.Api项目增添了风趣。 Swagger通过提供API的文档化规范以及方便进行测试和探索的UI极大地改善了我们的API开发体验-很难想象没有它就开发API。

现在,当我运行项目时,能够访问Swagger UI界面。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第3张图片

我在Swagger UI中填写了一些用户信息,并发送了Post请求。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第4张图片

我得到了成功的回应。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第5张图片

为了测试失败路径,我再次提交相同的请求,并收到400 Bad Request错误响应,告诉我该用户已经存在。使用手机或使用API​​的客户端可以轻松地以友好的方式提取和呈现此消息。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第6张图片

认证方式

我们可以使用我们的API创建用户;现在,我们将添加功能以对客户端进行身份验证,并向其颁发访问和刷新令牌。在以下步骤中,我们将使用身份API来验证用户凭据,并添加JWT中间件和其他位,以保护特定资源/ API免受未经授权的访问。

登录用例

顾名思义,LoginUseCase包含用于验证用户身份的重要逻辑。

public async TaskHandle(LoginRequest message, IOutputPort outputPort)
{
  if (!string.IsNullOrEmpty(message.UserName) && !string.IsNullOrEmpty(message.Password))
  {
    // ensure we have a user with the given user name
    var user = await _userRepository.FindByName(message.UserName);
    if (user != null)
    {
      // validate password
      if (await _userRepository.CheckPassword(user, message.Password))
      {
        // generate refresh token
        var refreshToken = _tokenFactory.GenerateToken();
        user.AddRereshToken(refreshToken, user.Id, message.RemoteIpAddress);
        await _userRepository.Update(user);
        // generate access token
        outputPort.Handle(new LoginResponse(await _jwtFactory.GenerateEncodedToken(user.IdentityId, user.UserName), refreshToken, true));
        return true;
       }
     }
  }
  outputPort.Handle(new LoginResponse(new[] { new Error("login_failure", "Invalid username or password.") }));
  return false;
}

让我们进一步细分一下。

初步调用:_userRepository.FindByName()和_userRepository.CheckPassword()使用基础用户存储库中的Identity API验证收到的用户凭据。

...
public async TaskFindByName(string userName)
{
  var appUser = await _userManager.FindByNameAsync(userName);
  return appUser == null ? null : _mapper.Map(
    appUser, 
    await GetSingleBySpec(new UserSpecification(appUser.Id))
    );
}

public async TaskCheckPassword(User user, string password)
{
  return await _userManager.CheckPasswordAsync(_mapper.Map(user), password);
}
...

如果一切顺利,我们将通过_tokenFactory.GenerateToken()生成一个新的刷新令牌。

internal sealed class TokenFactory : ITokenFactory
{
  public string GenerateToken(int size=32)
  {
    var randomNumber = new byte[size];
    using (var rng = RandomNumberGenerator.Create())
    {
       rng.GetBytes(randomNumber);
       return Convert.ToBase64String(randomNumber);
     }
  }
}

RandomNumberGenerator来自System.Security.Cryptography命名空间,并创建一个加密强化的随机值,我们将其用于刷新令牌。

生成新的令牌值后,我们使用User域实体上的AddRereshToken()方法将其发布给用户。

public void AddRereshToken(string token,int userId,string remoteIpAddress,double daysToExpire=5)
{
  _refreshTokens.Add(new RefreshToken(token, DateTime.UtcNow.AddDays(daysToExpire),userId, remoteIpAddress));
}

我们将其默认生存期设置为5天。很快,我们将看到在验证交换的刷新令牌期间在哪里检查此值。

最后,我们通过_jwtFactory.GenerateEncodedToke()生成一个新的JWT令牌,并将其通过输出端口进行管道传输,以将其作为Web API响应的一部分返回。

public async TaskGenerateEncodedToken(string id, string userName)
{
  var identity = GenerateClaimsIdentity(id, userName);

  var claims = new[]
  {
    new Claim(JwtRegisteredClaimNames.Sub, userName),
    new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
    new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
    identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol),
    identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Id)
  };

  // Create the JWT security token and encode it.
  var jwt = new JwtSecurityToken(
  _jwtOptions.Issuer,
  _jwtOptions.Audience,
  claims,
  _jwtOptions.NotBefore,
  _jwtOptions.Expiration,
  _jwtOptions.SigningCredentials);
  return new AccessToken(_jwtTokenHandler.WriteToken(jwt), (int)_jwtOptions.ValidFor.TotalSeconds);
}

在这里,我们将各种Claims添加到令牌中。这些是在JWT规范中具有保留名称的已注册名称和我们自己创建的公共名称的组合。

我们令牌中的已注册的claims包括:

  • iss:JWT发行者。
  • aud:JWT订阅者。
  • sub:JWT的主题。
  • exp:JWT的到期时间。
  • jti:JWT的唯一标识符。
  • iat:jwt时间发行。用于检查令牌的年龄。

公开的Claims是:

  • rol:用户在我们的API上下文中所扮演的角色。在针对控制器或控制器中的操作的基于角色的授权检查中使用此属性。
  • id:用户ID。在需要获取用户实体的场景中很有用。

最后一步是生成序列化的JWT,以传递回客户端。为此,我们使用_jwtTokenHandler.WriteToken(). _jwtTokenHandler’主要是System.IdentityModel.Tokens.Jwt命名空间中JwtSecurityTokenHandler`的包装,并包含签名密钥和JWT中间件提供的其他配置位。接下来,我们将看一下中间件的设置。

在基础结构项目的Auth文件夹中检查代码,以更详细地探索负责生成和验证JWT以及刷新令牌的类。

JWT Middleware

在我们可以在API中打开JWT之前,必须在ASP.NET Core管道中连接JWT中间件。 ASP.NET Core 2.1.0在Microsoft.AspNetCore.App程序包中包含所有必需的API。之后,所有必需的配置都在Startup.cs ConfigureServices()方法中执行。我已经在这里抽出了相关的部分。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
  // Register the ConfigurationBuilder instance of AuthSettings
  var authSettings = Configuration.GetSection(nameof(AuthSettings));
  services.Configure(authSettings);

  var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(authSettings[nameof(AuthSettings.SecretKey)]));

  // jwt wire up
  // Get options from app settings
  var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));

  // Configure JwtIssuerOptions
  services.Configure(options =>
  {
    options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
    options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
    options.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
  });

  var tokenValidationParameters = new TokenValidationParameters
  {
    ValidateIssuer = true,
    ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
    ValidateAudience = true,
    ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = signingKey,
    RequireExpirationTime = false,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
  };

  services.AddAuthentication(options =>
  {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  }).AddJwtBearer(configureOptions =>
  {
    configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
    configureOptions.TokenValidationParameters = tokenValidationParameters;
    configureOptions.SaveToken = true;
   });

   // api user claim policy
   services.AddAuthorization(options =>
   {
     options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol,  Constants.Strings.JwtClaims.ApiAccess));
   });
...
}

Auth Controller

接下来,我添加了一个带有Login()操作的AuthController,用于接收用户凭据并调用相关的用例。

// POST api/auth/login
[HttpPost("login")]
public async TaskLogin([FromBody] Models.Request.LoginRequest request)
{
  if (!ModelState.IsValid) { return BadRequest(ModelState); }
  await _loginUseCase.Handle(new LoginRequest(request.UserName, request.Password,   Request.HttpContext.Connection.RemoteIpAddress?.ToString()), _loginPresenter);
  return _loginPresenter.ContentResult;
}

测试登录端点

接下来,我添加了一个带有Login()操作的AuthController,用于接收用户凭据并调用相关的用例。

// POST api/auth/login
[HttpPost("login")]
public async TaskLogin([FromBody] Models.Request.LoginRequest request)
{
  if (!ModelState.IsValid) { return BadRequest(ModelState); }
  await _loginUseCase.Handle(new LoginRequest(request.UserName, request.Password,   Request.HttpContext.Connection.RemoteIpAddress?.ToString()), _loginPresenter);
  return _loginPresenter.ContentResult;
}

测试登录端点

我重新运行了该项目,并在Swagger UI中看到了新的登录端点-到目前为止一切顺利。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第7张图片

在请求正文中,我输入了先前创建的用户的凭据并执行了请求。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第8张图片

注意,在现实世界中,此请求必须通过HTTPS发出。为了提高安全性,您还可以对证书有效载荷进行base64编码。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第9张图片

我收到一个成功的响应,其中包括以下内容:

  • 一个accessToken是我们的JWT。我们将使用Bearer eyJhbGciOiJIUzI …格式将该值作为Authorization标头的一部分发送回后续请求中,以访问我们API的受保护资源。

  • expiresIn属性,是令牌有效的秒数。这是在JwtIssuerOptions中定义的。我们当前的设置是7200秒(120分钟)。

  • 客户端可以交换一个新的访问令牌的refreshToken。

通过基于角色的授权访问受保护的控制器

我们可以使用上一步中收到的JWT访问我们API的受保护路由。

我添加了一个ProtectedController,它执行的功能不多,但饰有一个执行ApiUser策略的Authorize属性。

[Authorize(Policy = "ApiUser")]
[Route("api/[controller]")]
[ApiController]
public class ProtectedController : ControllerBase
{
  // GET api/protected/home
  [HttpGet]
  public IActionResult Home()
  {
    return new OkObjectResult(new { result = true });
  }
}

该策略是在Startup.cs的ConfigureServices()中设置的,仅指示只有具有API访问权限的用户才能访问受保护的控制器或操作。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
...

通过向ProtectedController上的单个端点发出测试请求,我们可以使用Swagger UI来查看实际的策略。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第10张图片

该请求导致出现401未经授权的响应,因为我没有包含包含我的JWT的适当授权标头。让我们修复它。注意响应中的www-authenticate标头。服务器提供的一条线索可以让我们知道它期望我们使用哪种身份验证方案。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第11张图片

使用Swagger测试受保护的API轻而易举,因为它使我们能够定义API所需的各种身份验证和授权方案。

...
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
  c.SwaggerDoc("v1", new Info { Title = "AspNetCoreApiStarter", Version = "v1" });
  // Swagger 2.+ support
  c.AddSecurityDefinition("Bearer", new ApiKeyScheme
  {
    In = "header",
    Description = "Please insert JWT with Bearer into field",
    Name = "Authorization",
    Type = "apiKey"
  });

  c.AddSecurityRequirement(new Dictionary>
  {
    { "Bearer", new string[] { } }
  });
});
...

将安全配置添加到Swagger后,我们应该在Swagger UI页面顶部看到一个Authorize按钮。

swagger-ui-authorize-button

单击该按钮将启动“可用授权”对话框,在该对话框中,我使用Bearer {Token}格式输入了我在登录步骤中早些时候收到的JWT令牌的授权标头值。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第12张图片

创建了auth标头后,就Swagger而言,我现在已“登录”。我将测试请求重新发送到ProtectedController,并收到200成功响应-JWT授权正在工作。

交换刷新令牌

我们在API中建立了功能,用于创建新的用户帐户,向他们颁发访问和刷新令牌以及授权对受保护资源的访问。现在,我们将添加将已过期的JWT令牌交换为新令牌的功能。

Exchange刷新令牌用例

再一次,我们将从用例开始,然后从那里开始。

public async TaskHandle(ExchangeRefreshTokenRequest message,IOutputPort outputPort)
{
  var cp = _jwtTokenValidator.GetPrincipalFromToken(message.AccessToken, message.SigningKey);

  // invalid token/signing key was passed and we can't extract user claims
  if (cp != null)
  {
    var id = cp.Claims.First(c => c.Type == "id");
    var user = await _userRepository.GetSingleBySpec(new UserSpecification(id.Value));
    if (user.HasValidRefreshToken(message.RefreshToken))
    {
      var jwtToken = await _jwtFactory.GenerateEncodedToken(user.IdentityId, user.UserName);
      var refreshToken = _tokenFactory.GenerateToken();
      user.RemoveRefreshToken(message.RefreshToken); // delete the token we've exchanged
      user.AddRereshToken(refreshToken, user.Id, ""); // add the new one
      await _userRepository.Update(user);
      outputPort.Handle(new ExchangeRefreshTokenResponse(jwtToken, refreshToken, true));
      return true;
    }
}
  outputPort.Handle(new ExchangeRefreshTokenResponse(false, "Invalid token."));
  return false;
}

第一步,我们使用_jwtTokenValidator.GetPrincipalFromToken()来验证接收到的访问令牌。如果我们拥有有效的JWT,则会从ID声明中提取用户ID,然后从数据库中提取用户。我们通过比较令牌值和Active标志,在User实体上使用一种方法来检查刷新令牌的有效性-非常简单。

public bool HasValidRefreshToken(string refreshToken)
{
  return _refreshTokens.Any(rt => rt.Token == refreshToken && rt.Active);
}

如果刷新令牌有效,我们将执行以下步骤来完成交换:

  • 通过_jwtFactory.GenerateEncodedToken()创建一个新的JWT。
  • 通过_tokenFactory.GenerateToken()创建一个新的刷新令牌。
  • 通过user.RemoveRefreshToken()删除用户的旧令牌。这一点很重要!
  • 通过_userRepository.Update()添加用户的新刷新令牌。
  • 将更改保存在数据库中,并通过输出端口传递新令牌。

刷新令牌控制器操作

有了用例之后,我回到了Web API项目,并使用新的RefreshToken操作扩展了AuthController,该操作允许匿名访问,并期望接收访问并刷新令牌作为输入。

// POST api/auth/refreshtoken
[HttpPost("refreshtoken")]
public async TaskRefreshToken([FromBody] Models.Request.ExchangeRefreshTokenRequest request)
{
  if (!ModelState.IsValid) { return BadRequest(ModelState);}
  await _exchangeRefreshTokenUseCase.Handle(new ExchangeRefreshTokenRequest(request.AccessToken, request.RefreshToken, _authSettings.SecretKey), _exchangeRefreshTokenPresenter);
  return _exchangeRefreshTokenPresenter.ContentResult;
}

客户端令牌到期工作流程

The most significant benefit refresh tokens offer from the perspective of the user is the seamless experience it creates by preventing the need for them to log in again. For this to happen, the client must realize when its access token is expired and act accordingly.
从用户的角度来看,刷新令牌提供的最显着的好处是通过避免再次登录而带来的无缝体验。为此,客户端必须意识到其访问令牌何时到期并采取相应措施。

交换刷新令牌的典型客户端工作流程可能如下所示:

  • 客户端使用过期的令牌向受保护的资源发出请求,并接收包含Token-Expired标头的响应。
  • 检测到过期的令牌后,它将请求发送到刷新端点,同时传递过期的访问令牌及其刷新令牌以进行验证。
  • 如果验证成功,则客户端将接收新的访问和刷新令牌。
  • 客户端使用新令牌重试原始请求,然后重复该循环。

根据要构建的客户端类型,这些步骤的实现将有所不同。 SPA,移动。一个真实的例子对于将来的博客文章来说将是一个很好的话题,但是到目前为止,我们可以使用Swagger测试此流程。

测试刷新令牌端点

目前,在我们的演示项目中,JWT的生命周期在JwtIssuerOptions中每2小时进行一次硬编码。我之前收到的令牌现在已过期,因此当我尝试访问受保护的路由时,响应中的令牌过期标头会显示401 Unauthorized。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第13张图片

在真实世界的客户端中,令牌已过期的标头是我们的应用程序需要拦截以触发对刷新端点的请求的信号。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第14张图片

我在Swagger中通过将过期的访问令牌和刷新令牌粘贴到请求正文中来测试刷新令牌端点,从而再次模拟了此步骤。

ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程_第15张图片

我提交了请求,瞧-我收到了包含新访问和刷新令牌的成功回复!

总结

我们在这里介绍了很多内容,希望您在本指南中发现了一些价值。免责声明:这些代码示例尚未投入生产,代码中有配置,密钥存储不安全等,因此请注意这一点,并确保您在项目中实现的任何位或概念都符合项目的安全要求。

如果您有任何意见,改进或问题,请在评论中让我知道!

源码

你可能感兴趣的:(asp.net,core)