代码之家  ›  专栏  ›  技术社区  ›  Matthew Flynn

使用Blazor实现具有刷新令牌的短期Jwt

  •  0
  • Matthew Flynn  · 技术社区  · 6 年前

    我们目前正在开发 Blazor 使用具有刷新令牌的短期(10分钟)Jwt保护的应用程序。

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

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

    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] 属性)。页面最初运行良好,并在标头中发送Jwt。然而,如果我 f5 刷新页面我在 /fecthData 端点,即在代码上;

    @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,然后以这种方式续订令牌?

    0 回复  |  直到 6 年前
        1
  •  6
  •   dani herrera    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