使用 Blazor 实现带有刷新令牌的短暂 Jwt
Implementing short lived Jwt with Refresh Token with Blazor
我们目前正在开发一个 Blazor
应用程序,该应用程序使用短期(10 分钟)Jwt 和刷新令牌进行保护。
目前我们已经实现了 Jwt,通过 Blazor 服务器端 Web api 可以登录,生成 Jwt 并生成刷新令牌。
我在客户端使用了以下 link;
Authentication With client-side Blazor
并扩展 ApiAuthenticationStateProvider.cs
如下;
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken");
if (string.IsNullOrWhiteSpace(savedToken) || string.IsNullOrWhiteSpace(refreshToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", savedToken);
if(userResponse.HasError)
{
var response = await _httpClient.PostAsync<LoginResponse>("api/login/refreshToken", new RefreshTokenModel { RefreshToken = refreshToken });
//check result now
if (!response.HasError)
{
await _localStorage.SetItemAsync("authToken", response.Result.AccessToken);
await _localStorage.SetItemAsync("refreshToken", response.Result.RefreshToken);
userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", response.Result.AccessToken);
}
}
var identity = !userResponse.HasError ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userResponse.Result.Email) }, "apiauth") : new ClaimsIdentity();
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}
因此,如果 Jwt 第一次失败,我们会尝试使用刷新令牌进行更新。
上面的代码可以正常工作,但是我发现的第一个问题是,如果我随后导航到 /fetchData
测试终点(受 [Authorize]
属性保护)。该页面最初运行良好,并在 header 中发送 Jwt。但是,如果我然后 f5
并刷新页面,我会在 /fecthData
端点上获得 401 未经授权,即在代码上;
@code {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
现在,如果要解决这个问题,我可以手动将 Jwt 表单 localStorage 添加到 header(在我的例子中,我使用扩展方法);
public static async Task<ServiceResponse<T>> GetAsync<T>(
this HttpClient httpClient, string url, string token)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
var response = await httpClient.GetAsync(url);
return await BuildResponse<T>(response);
}
但是,我遇到的第二个问题是,如果 Jwt 在此调用期间过期,我将需要调用以使用刷新令牌来获取新的 Jwt。
有没有一种方法可以使用中间件执行此操作,以避免在每次调用时检查 401,然后以这种方式更新令牌?
我们经常将 Blazor 视为 MVC,但事实并非如此。它更像是浏览器中的桌面应用程序 运行。我以这种方式使用 JWT 和更新令牌:登录后,我有一个无限循环,它正在 ping 后端并保持会话并更新令牌。简化:
class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
private bool IsLogedIn = false;
private CustomCredentials credentials = null;
// private ClaimsPrincipal currentClaimsPrincipal = null; (optinally)
public Task Login( string user, string password )
{
credentials = go_backend_login_service( user, password );
// do stuff with credentials and claims
// I raise event here to notify login
keepSession( );
}
public Task Logout( )
{
go_bakcend_logout_service( credentials );
// do stuff with claims
IsLogedIn = false;
// I raise event here to notify logout
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
// make a response from credentials or currentClaimsPrincipal
}
private async void KeepSession()
{
while(IsLogedIn)
{
credentials = go_backend_renewingJWT_service( credentials );
// do stuff with new credentials: check are ok, update IsLogedIn, ...
// I raise event here if server says logout
await Task.Delay(1000); // sleep for a while.
}
}
}
记得DI注册组件:
public void ConfigureServices(IServiceCollection services)
{
// ... other services added here ...
// One JWTAuthenticationStateProvider for each connection on server side.
// A singleton for clientside.
services.AddScoped<AuthenticationStateProvider,
JWTAuthenticationStateProvider>();
}
这只是一个想法,您应该考虑一下并根据自己的解决方案进行调整。
的身份验证和授权的更多信息
我们目前正在开发一个 Blazor
应用程序,该应用程序使用短期(10 分钟)Jwt 和刷新令牌进行保护。
目前我们已经实现了 Jwt,通过 Blazor 服务器端 Web api 可以登录,生成 Jwt 并生成刷新令牌。
我在客户端使用了以下 link;
Authentication With client-side Blazor
并扩展 ApiAuthenticationStateProvider.cs
如下;
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken");
if (string.IsNullOrWhiteSpace(savedToken) || string.IsNullOrWhiteSpace(refreshToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", savedToken);
if(userResponse.HasError)
{
var response = await _httpClient.PostAsync<LoginResponse>("api/login/refreshToken", new RefreshTokenModel { RefreshToken = refreshToken });
//check result now
if (!response.HasError)
{
await _localStorage.SetItemAsync("authToken", response.Result.AccessToken);
await _localStorage.SetItemAsync("refreshToken", response.Result.RefreshToken);
userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", response.Result.AccessToken);
}
}
var identity = !userResponse.HasError ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userResponse.Result.Email) }, "apiauth") : new ClaimsIdentity();
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}
因此,如果 Jwt 第一次失败,我们会尝试使用刷新令牌进行更新。
上面的代码可以正常工作,但是我发现的第一个问题是,如果我随后导航到 /fetchData
测试终点(受 [Authorize]
属性保护)。该页面最初运行良好,并在 header 中发送 Jwt。但是,如果我然后 f5
并刷新页面,我会在 /fecthData
端点上获得 401 未经授权,即在代码上;
@code {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
现在,如果要解决这个问题,我可以手动将 Jwt 表单 localStorage 添加到 header(在我的例子中,我使用扩展方法);
public static async Task<ServiceResponse<T>> GetAsync<T>(
this HttpClient httpClient, string url, string token)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
var response = await httpClient.GetAsync(url);
return await BuildResponse<T>(response);
}
但是,我遇到的第二个问题是,如果 Jwt 在此调用期间过期,我将需要调用以使用刷新令牌来获取新的 Jwt。
有没有一种方法可以使用中间件执行此操作,以避免在每次调用时检查 401,然后以这种方式更新令牌?
我们经常将 Blazor 视为 MVC,但事实并非如此。它更像是浏览器中的桌面应用程序 运行。我以这种方式使用 JWT 和更新令牌:登录后,我有一个无限循环,它正在 ping 后端并保持会话并更新令牌。简化:
class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
private bool IsLogedIn = false;
private CustomCredentials credentials = null;
// private ClaimsPrincipal currentClaimsPrincipal = null; (optinally)
public Task Login( string user, string password )
{
credentials = go_backend_login_service( user, password );
// do stuff with credentials and claims
// I raise event here to notify login
keepSession( );
}
public Task Logout( )
{
go_bakcend_logout_service( credentials );
// do stuff with claims
IsLogedIn = false;
// I raise event here to notify logout
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
// make a response from credentials or currentClaimsPrincipal
}
private async void KeepSession()
{
while(IsLogedIn)
{
credentials = go_backend_renewingJWT_service( credentials );
// do stuff with new credentials: check are ok, update IsLogedIn, ...
// I raise event here if server says logout
await Task.Delay(1000); // sleep for a while.
}
}
}
记得DI注册组件:
public void ConfigureServices(IServiceCollection services)
{
// ... other services added here ...
// One JWTAuthenticationStateProvider for each connection on server side.
// A singleton for clientside.
services.AddScoped<AuthenticationStateProvider,
JWTAuthenticationStateProvider>();
}
这只是一个想法,您应该考虑一下并根据自己的解决方案进行调整。
的身份验证和授权的更多信息