JWT 不会使用 Blazor 存储在 ASP.NET Core 中

JWT doesn't get stored in ASP.NET Core with Blazor

我遵循了这个教程:https://medium.com/@st.mas29/microsoft-blazor-web-api-with-jwt-authentication-part-1-f33a44abab9d

我下载了示例:https://github.com/StuwiiDev/DotnetCoreJwtAuthentication/tree/Part2

我可以看到令牌已创建,但我不明白它是如何或应该如何保存在客户端的,因为每次我访问 SampleDataController,其中有 Authorize标记,它 returns 一个 401。

使用 Postman 调用和添加令牌时有效。

要对我的用户进行身份验证,我缺少什么? Microsoft.AspNetCore.Authentication.JwtBearer 不处理客户端部分(存储令牌)吗?

What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?

JwtBearer在服务器端运行,它只会验证请求的授权头,即Authorization: Bearer your_access_token,而不会关心你的WebAssembly代码如何运行。因此,您需要使用 jwt accessToken 发送请求。由于本教程建议您应该使用 localStorage ,让我们将 accessTokenlocalStorage 一起存储。

因为 WebAssembly 还没有访问 BOM 的权限,我们需要一些 javascript 代码作为胶水。为此,在 JwtAuthentication.Client/wwwroot/js/ 下添加一个 helper.js :

var wasmHelper = {};

wasmHelper.ACCESS_TOKEN_KEY ="__access_token__";

wasmHelper.saveAccessToken = function (tokenStr) {
    localStorage.setItem(wasmHelper.ACCESS_TOKEN_KEY,tokenStr);
};

wasmHelper.getAccessToken = function () {
    return localStorage.getItem(wasmHelper.ACCESS_TOKEN_KEY);
};

并在您的 JwtAuthentication.Client/wwwroot/index.html

中引用脚本
<body>
    <app>Loading...</app>
    <script src="js/helper.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

现在,让我们将 javascript 代码包装到 C# 中。创建一个新文件 Client/Services/TokenService.cs:

public class TokenService
{
    public Task SaveAccessToken(string accessToken) {
        return JSRuntime.Current.InvokeAsync<object>("wasmHelper.saveAccessToken",accessToken);
    }
    public Task<string> GetAccessToken() {
        return JSRuntime.Current.InvokeAsync<string>("wasmHelper.getAccessToken");
    }
}

通过以下方式注册此服务:

// file: Startup.cs 
services.AddSingleton<TokenService>(myTokenService);

现在我们可以将 TokenService 注入 Login.cshtml 并用它来保存令牌:

@using JwtAuthentication.Client.Services
// ...
@page "/login"
// ...
@inject TokenService tokenService

// ...

@functions {
    public string Email { get; set; } = "";
    public string Password { get; set; } = "";
    public string Token { get; set; } = "";


    /// <summary>
    /// response from server
    /// </summary>
    private class TokenResponse{
        public string Token;
    }

    private async Task SubmitForm()
    {
        var vm = new TokenViewModel
        {
            Email = Email,
            Password = Password
        };

        var response = await Http.PostJsonAsync<TokenResponse>("http://localhost:57778/api/Token", vm);
        await tokenService.SaveAccessToken(response.Token);
    }
}

假设您要在 FetchData.cshtml

内发送数据
@functions {
    WeatherForecast[] forecasts;


    protected override async Task OnInitAsync()
    {
        var token = await tokenService.GetAccessToken();
        Http.DefaultRequestHeaders.Add("Authorization",String.Format("Bearer {0} ",token));
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
    }
}

结果将是:

以下 class 处理客户端上的登录过程,将 JWT 令牌存储在 local 存储中。注意:开发人员有责任存储 JWT 令牌并将其传递给服务器。客户端(Blazor、Angular 等)不会自动为他执行此操作。

public class SignInManager
    {
        // Receive 'http' instance from DI
        private readonly HttpClient http;
        public SignInManager(HttpClient http)
        {
            this.http = http;
        }

        [Inject]
        protected LocalStorage localStorage;


        public bool IsAuthenticated()
        {
            var token = localStorage.GetItem<string>("token");

            return (token != null); 
        }

        public string getToken()
        {
            return localStorage.GetItem<string>("token");
        }

        public void Clear()
        {
            localStorage.Clear();
        }


        // model.Email, model.Password, model.RememberMe, lockoutOnFailure: false
        public async Task<bool> PasswordSignInAsync(LoginViewModel model)
        {
            SearchInProgress = true;
            NotifyStateChanged();

            var result = await http.PostJsonAsync<Object>("/api/Account", model);

            if (result)// result.Succeeded
           {
              _logger.LogInformation("User logged in.");

              // Save the JWT token in the LocalStorage
              // https://github.com/BlazorExtensions/Storage
              await localStorage.SetItem<Object>("token", result);


              // Returns true to indicate the user has been logged in and the JWT token 
              // is saved on the user browser
             return true;

           }

        }
    }

// 这就是您调用 Web API 的方式,向它发送 // 当前用户的 JWT 令牌

public async Task<IList<Profile>> GetProfiles()
        {   
            SearchInProgress = true;
            NotifyStateChanged();

            var token = signInManager.getToken();
            if (token == null) {
                throw new ArgumentNullException(nameof(AppState)); //"No token";
            }

            this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            // .set('Content-Type', 'application/json')
            // this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            Profiles = await this.http.GetJsonAsync<Profile[]>("/api/Profiles");


            SearchInProgress = false;
            NotifyStateChanged();
        } 

// 您还必须在客户端上设置 Startup class,如下所示:

public void ConfigureServices(IServiceCollection services)
    {
        // Add Blazor.Extensions.Storage
       // Both SessionStorage and LocalStorage are registered
       // https://github.com/BlazorExtensions/Storage
       **services.AddStorage();**

      ...
    }

// 一般而言,这是您必须在客户端上执行的操作。 // 在服务器上,你必须有一个方法,比如在 Account 控制器中,它的功能是生成 JWT 令牌,你必须配置 JWT 中间件,用必要的属性注释你的控制器,至于实例:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]  

等等...

希望这对您有所帮助...

提前致歉,因为这在某种程度上回应了之前的回答,但我没有代表对此发表评论。

如果它能帮助其他同样在寻找在 Blazor 应用程序中使用 JWT 的解决方案的人,我发现 @itminus 的回答非常有用,但它也让我想到了另一门课程。

我发现的一个问题是第二次调用 FetchData.cshtml 时会在尝试添加 Authorization header 时崩溃。

我没有在此处添加默认值 header,而是在成功登录后将其添加到 HttpClient 单例(我相信 Blazor 会自动为您创建)。因此,从@itminus 的回答中更改 Login.cshtml 中的 SubmitForm

    protected async Task SubmitForm()
    {
        // Remove any existing Authorization headers
        Http.DefaultRequestHeaders.Remove("Authorization");

        TokenViewModel vm = new TokenViewModel()
        {
            Email = Email,
            Password = Password
        };

        TokenResponse response = await Http.PostJsonAsync<TokenResponse>("api/Token/Login", vm);

        // Now add the token to the Http singleton
        Http.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0} ", response.Token));
    }

然后我意识到,在构建 SPA 时,我根本不需要跨请求保留令牌 - 它只是附加到 HttpClient。