JWT не сохраняется в ASP.NET Core с 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, он возвращает 401.

При вызове и добавлении токена с помощью Postman работает.

Что мне не хватает для аутентификации моего пользователя? Разве Microsoft.AspNetCore.Authentication.JwtBearer не обрабатывает клиентскую часть (хранит токен)?


person Westerlund.io    schedule 07.10.2018    source источник
comment
Вы используете Authorize в качестве имени в заголовках? Если да, то должно быть Authorization.   -  person Brad    schedule 08.10.2018
comment
Этого я не понимаю. Я выполнил руководство, загрузил проект и ожидал увидеть токен после входа в систему. Как установить этот токен? JwtBearer не делает этого для меня, я полагаю, согласно вашему комментарию?   -  person Westerlund.io    schedule 08.10.2018
comment
Я понимаю ваше замешательство ... похоже, в учебнике не используется заголовок Authorization, но токен предоставляется в качестве параметра строки запроса. Автор показывает, как использовать его для тестирования в Postman, но не в клиентском приложении. В конце части 2 он упоминает, что вы можете сохранить токен в localStorage, но не приводит пример фактического выполнения запроса от клиента к серверу с использованием токена. Это не очень хороший или полезный учебник, ИМО.   -  person Brad    schedule 08.10.2018


Ответы (3)


Что мне не хватает для аутентификации моего пользователя? Разве Microsoft.AspNetCore.Authentication.JwtBearer не обрабатывает клиентскую часть (хранит токен)?

JwtBearer работает на стороне сервера, он проверяет только заголовок авторизации запроса, а именно Authorization: Bearer your_access_token, и не заботится о том, как работают ваши коды WebAssembly. Поэтому вам нужно отправить запрос с токеном доступа jwt. Поскольку в руководстве предлагается использовать localStorage, давайте сохраним accessToken с localStorage.

Поскольку WebAssembly еще не имеет доступа к BOM, нам нужны некоторые коды javascript, которые могут послужить связующим звеном. Для этого добавьте helper.js под JwtAuthentication.Client/wwwroot/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");
    }
}

и результат будет:

введите описание изображения здесь

person itminus    schedule 08.10.2018
comment
Спасибо за отличное объяснение, теперь оно работает! Для записи: чтобы иметь возможность ввести TokenService, мне нужно было добавить его в службу в Startup.ConfigureServices - services.AddSingleton<TokenService>(myTokenService); - person Westerlund.io; 09.10.2018
comment
Отличный ответ, действительно полезный. Также очень полезный комментарий выше. Я бы порекомендовал указать его в ответе, так как без него служба токенов не может быть вызвана. - person George Harnwell; 23.07.2019
comment
@GeorgeHarnwell Спасибо за ваше предложение, я отредактировал свой ответ :) - person itminus; 24.07.2019

Заранее приносим свои извинения, так как это отчасти соответствует предыдущему ответу, но у меня нет представителя, чтобы это прокомментировать.

Если это поможет кому-то еще, кто так же искал решение для использования JWT в приложении Blazor, я нашел ответ @itminus невероятно полезным, но он также указал мне на другой курс.

Одна проблема, которую я обнаружил, заключалась в том, что вызов FetchData.cshtml во второй раз приводил к сбою, когда он пытался добавить заголовок Authorization во второй раз.

Вместо того, чтобы добавлять туда заголовок по умолчанию, я добавил его в синглтон HttpClient после успешного входа в систему (который, как мне кажется, Blazor создает для вас автоматически). Итак, изменив SubmitForm в Login.cshtml из ответа @itminus.

    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.

person Oliver    schedule 14.11.2018

Следующий класс обрабатывает процесс входа в систему на клиенте, сохраняя токен 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;

           }

        }
    }

// Вот как вы вызываете свой веб-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 на клиенте следующим образом:

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

      ...
    }

// Вообще говоря, это то, что вам нужно сделать на клиенте. // На сервере у вас должен быть метод, скажем, в контроллере учетной записи, функция которого заключается в генерации токена JWT, вы должны настроить промежуточное программное обеспечение JWT, чтобы аннотировать ваши контроллеры с помощью необходимого атрибута, как для пример:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]  

и так далее...

Надеюсь это поможет...

person enet    schedule 08.10.2018