4

我们目前正在开发一个Blazor应用程序,该应用程序使用带有刷新令牌的短期(10 分钟)Jwt 进行保护。

目前我们已经实现了 Jwt,并且可以通过 Blazor 服务器端 Web api 登录、生成 Jwt 并生成刷新令牌。

从客户端我使用了以下链接;

使用客户端 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]属性保护)。该页面最初运行良好并在标头中发送 Jwt。但是,如果我然后f5刷新页面,我会在/fecthData端点上得到 401 未授权,即在代码上;

@code {
    WeatherForecast[] forecasts;

    protected override async Task OnInitAsync()
    {
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
    }
} 

现在,如果要解决这个问题,我可以手动将 Jwt 表单 localStorage 添加到标题中(在我的情况下,我使用扩展方法);

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,然后以这种方式更新令牌?

4

1 回答 1

6

我们经常将 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>();
}

这只是一个想法,您应该考虑它并使其适应您自己的解决方案。

有关github SteveSandersonMS/blazor-auth.md上的身份验证和授权的更多信息

于 2019-08-24T15:59:09.057 回答