ASP.NET Core Web API中帶有刷新令牌的JWT身份驗證流程
翻譯自:地址
在今年年初,我整理了有關將JWT身份驗證與ASP.NET Core Web API和Angular一起使用的詳細指南。目前有120多個評論,它是Internet上這個角落中最繁忙的頁面,這可能表明許多開發人員在連接身份驗證時面臨的挑戰。
如果我不得不選擇該帖子中缺少的一項重要內容,那可能是刷新令牌及其在JWT身份驗證和授權工作流程中的微妙而必不可少的角色。我認為將那篇文章中的Web API項目更新和重組為一個包含刷新令牌的獨立JWT解決方案是值得的-因此,請為您喜歡的飲料重新裝瓶,然后開始吧。 ☕️🍺
JWT復習
現代的身份驗證和授權協議使用令牌作為僅攜帶足夠數據以授權用戶執行操作或從資源請求數據的方法。簡而言之,令牌是允許進行某些授權過程的信息包。 JWT令牌特別提供了一種非常方便的方式,以權利要求的形式打包有關用戶的通用屬性。關於聲明的好處是,它們可以被信任並可以反復驗證,因為在大多數情況下,它們是使用帶有HMAC算法的私鑰進行數字簽名的。該簽名確保只有擁有密鑰的服務器才能解碼和驗證傳入令牌的內容,並授予或拒絕對其資源的訪問。
刷新令牌
上圖在說明身份驗證服務器如何獲取訪問令牌,然后如何在隨后的訪問受保護資源的請求中交換訪問令牌方面相對簡單。但是,如果我們對它進行足夠長的研究,我們應該提出一個關鍵問題:訪問令牌的生存期有多長?它會持續一個小時,一天或一個月嗎?
這個問題很重要,因為如果某些惡意方要持有令牌,那么他們可以在冒充真實接收者的同時,終身使用令牌。發生這種情況是因為服務器將始終信任帶有有效簽名的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<AppIdentityDbContext>
{
protected override AppIdentityDbContext CreateNewInstance(DbContextOptions <AppIdentityDbContext> options)
{
return new AppIdentityDbContext(options);
}
}
我們需要做的下一件事是在ASP.NET Core中間件中連接身份提供程序。我在Startup.cs的ConfigureServices()方法中添加了必要的配置。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<AppIdentityDbContext>(
options => options.UseSqlServer(
Configuration.GetConnectionString("Default"),
b => b.MigrationsAssembly("Web.Api.Infrastructure"))
);
...
// add identity
var identityBuilder = services.AddIdentityCore<AppUser>(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<CreateUserResponse> 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<bool> Handle(RegisterUserRequest message, IOutputPort<RegisterUserResponse> 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<ActionResult> 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界面。
我在Swagger UI中填寫了一些用戶信息,並發送了Post請求。
我得到了成功的回應。 😎
為了測試失敗路徑,我再次提交相同的請求,並收到400 Bad Request錯誤響應,告訴我該用戶已經存在。使用手機或使用API的客戶端可以輕松地以友好的方式提取和呈現此消息。
認證方式
我們可以使用我們的API創建用戶;現在,我們將添加功能以對客戶端進行身份驗證,並向其頒發訪問和刷新令牌。在以下步驟中,我們將使用身份API來驗證用戶憑據,並添加JWT中間件和其他位,以保護特定資源/ API免受未經授權的訪問。
登錄用例
顧名思義,LoginUseCase包含用於驗證用戶身份的重要邏輯。
public async Task<bool>Handle(LoginRequest message, IOutputPort<LoginResponse> 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 Task<User>FindByName(string userName)
{
var appUser = await _userManager.FindByNameAsync(userName);
return appUser == null ? null : _mapper.Map(
appUser,
await GetSingleBySpec(new UserSpecification(appUser.Id))
);
}
public async Task<bool>CheckPassword(User user, string password)
{
return await _userManager.CheckPasswordAsync(_mapper.Map<AppUser>(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 Task<AccessToken>GenerateEncodedToken(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>(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<JwtIssuerOptions>(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 Task<ActionResult>Login([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 Task<ActionResult>Login([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中看到了新的登錄端點-到目前為止一切順利。 😎
在請求正文中,我輸入了先前創建的用戶的憑據並執行了請求。
🔐注意,在現實世界中,此請求必須通過HTTPS發出。為了提高安全性,您還可以對證書有效載荷進行base64編碼。
我收到一個成功的響應,其中包括以下內容:
-
一個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來查看實際的策略。
該請求導致出現401未經授權的響應,因為我沒有包含包含我的JWT的適當授權標頭。讓我們修復它。注意響應中的www-authenticate標頭。服務器提供的一條線索可以讓我們知道它期望我們使用哪種身份驗證方案。
使用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<string, IEnumerable<string>>
{
{ "Bearer", new string[] { } }
});
});
...
將安全配置添加到Swagger后,我們應該在Swagger UI頁面頂部看到一個Authorize按鈕。
單擊該按鈕將啟動“可用授權”對話框,在該對話框中,我使用Bearer {Token}格式輸入了我在登錄步驟中早些時候收到的JWT令牌的授權標頭值。
創建了auth標頭后,就Swagger而言,我現在已“登錄”。我將測試請求重新發送到ProtectedController,並收到200成功響應-JWT授權正在工作。 🤘
交換刷新令牌
我們在API中建立了功能,用於創建新的用戶帳戶,向他們頒發訪問和刷新令牌以及授權對受保護資源的訪問。現在,我們將添加將已過期的JWT令牌交換為新令牌的功能。
Exchange刷新令牌用例
再一次,我們將從用例開始,然后從那里開始。
public async Task<bool>Handle(ExchangeRefreshTokenRequest message,IOutputPort<ExchangeRefreshTokenResponse> 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 Task<ActionResult>RefreshToken([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。
在真實世界的客戶端中,令牌已過期的標頭是我們的應用程序需要攔截以觸發對刷新端點的請求的信號。
我在Swagger中通過將過期的訪問令牌和刷新令牌粘貼到請求正文中來測試刷新令牌端點,從而再次模擬了此步驟。
我提交了請求,瞧-我收到了包含新訪問和刷新令牌的成功回復!
總結
我們在這里介紹了很多內容,希望您在本指南中發現了一些價值。免責聲明:這些代碼示例尚未投入生產,代碼中有配置,密鑰存儲不安全等,因此請注意這一點,並確保您在項目中實現的任何位或概念都符合項目的安全要求。
如果您有任何意見,改進或問題,請在評論中讓我知道!