BlazorNr. 2 W poprzednim wpisie stworzyliśmy REST API z ASP.NET CORE w .NET 5/6 wraz z możliwością logowania użytkowników przez JSON WEB Tokeny.

Jeśli interesuje Cię ta część to proszę spójrz na ten wpis.

Nasz projekt wygląda tak:

Dodajemy Autoryzację: Samurai.Application.Security w Visual Studio

Teraz zobaczymy jak obsłużyć REST API z JWT służące nam do uwierzytelnienia w aplikacji Blazor. 

Visual Studio Blazor UI Warstwa

Jakie paczki NuGet ?

Od czego tutaj zacząć ? Otóż do nowego projektu trzeba dodać pewne paczki NuGet.

blazor webassembly

Na pewno będziemy potrzebowali paczki, która będzie nam deserializowała JSON z odpowiedzi HTTP. Klasy, które później wygenerujemy będą korzystać z tej paczki

Newtonsoft.Json paczka NuGet

Będziemy także potrzebować paczki "Microsoft.Extensions.Http", aby mieć większą kontrolę nad tworzeniem klasy HTTPClient.

Microsoft.Extensions.Http paczka NuGet

Skoro używamy autoryzacji to potrzebujemy paczki "Microsoft.AspNetCore.Authorization", która została stworzona z myślą o Blazor.

Microsoft.AspNetCore.Authorization paczka NuGet

Przejdźmy do naszych klas modeli.

ViewModels i Mapowanie

Jak pamiętasz z poprzedniego przykładu nasze REST API nie jest bogate w klasy. To dobrze, ponieważ nie musimy do nich tworzyć klasy "ViewModel".

O co chodzi? Otóż, aby moje projekty były bardziej elastyczne i niezależne od siebie to chce, aby każda obiekt, który wyjdzie z jako odpowiedź z REST API miał swoją klasę przeznaczoną do wyświetlania tych danych tylko po stronie widoku.

Jeśli coś zmienię w REST API np. definicję Warrior to szanse, że coś mi eksploduje po stronie UI Blazor będą dużo bardziej zmniejszone.

Oto wszystkie nasze ViewModele. Jak widzisz po stronie widoku będę chciał wyświetlić Użytkownika, przekazać dane do logowania oraz wyświetlić moich wojowników.

public class LoginBlazorVM
{
    public string Email { get; set; }
    public string Password { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public string Token { get; set; }
}

public class WarriorViewModel
{
    public string Name { get; set; }
    public string ImageUrl { get; set; }
    public int Age { get; set; }
}

Projekt jest mały, ale możemy skorzystać z AutoMapper, który automatycznie nam z mapuje nam obiekty klas, które wyjdą z REST API na nasze ViewModele.

Więcej o AutoMapper pisałem tutaj

Instalujemy odpowiednie paczki NuGet

paczka Nuget AutoMapper

Tworzymy definicję mapowania naszego wojownika w obie strony.

public MappingProfile()
{
    CreateMap<WarriorViewModel, Warrior>().ReverseMap();
}

A na końcu do klasy Program.cs dodajemy rejestracje naszego AutoMappera.

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");
    
    //builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

Teraz gdy to mamy możemy przejść do najtrudniejszej części. Czyli do tego, jak te dane będziemy pobierać z REST API.

Visual Studio projekt Blazor

A może tak wygenerować sobie metody do HTTPClient

Gdy pisałem ten projekt na potrzeby prelekcji na Warszawskie Dni Informatyki naprawdę nie chciało mi się ręcznie pisać wywołań HTTPClinet do mojego REST API w aplikacji Blazor.

Na szczęście możemy wygenerować taki kod, ponieważ nasze REST API używa Swagger do dokumentacji i testu.

Na pomoc przychodzi NSwag narzędzie, które pozwoli nam wygenerować API 

Co to jest NSwag ? Generator kodu

Instalator do tego programu znajdziesz tutaj : https://github.com/RicoSuter/NSwag/wiki/NSwagStudio

Korzystanie z tego narzędzia jest bardzo proste. Podaje w odpowiednim miejscu definicję JSON swojego Swaggera, a po drugiej stronie mogę zobaczyć wygenerowane wywołanie HTTPClient do tych metod w moim REST API

NSwagStudio program

Wygenerowana klasa jednak nie wyręcza mnie we wszystkim. Muszę ją trochę przerobić, aby JSON WEB Tokeny działały. Tego fragmentu kodu nie jestem w stanie wygenerować.

Do wygenerowanego interfejsu  IClient muszę dodać właściwość, która wystawi mi do użytku instancje HttpClienta

public partial interface IClient
{
    System.Net.Http.HttpClient HttpClient { get; }

...

Potem w wygenerowanej klasie muszę w konstruktorze dodać wstrzyknięcie tego HTTPClienta tak, aby miał nad nim większą kontrolę. To jest moja modyfikacja wygenerowanego kodu.

public partial class Client : IClient
{
    private string _baseUrl = "";
    private System.Net.Http.HttpClient _httpClient;
    private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
    
    public Client(string baseUrl, System.Net.Http.HttpClient httpClient)
    {
        BaseUrl = baseUrl; 
        _httpClient = httpClient; 
        _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
    }

Dodatkowo z konstruktora wywalam baseUrl, gdyż tak naprawdę utrudnia mi to operację wstrzykiwania zależności. 

[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class Client : IClient
{
    private string _baseUrl = "";
    private System.Net.Http.HttpClient _httpClient;
    private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
    
    public Client(System.Net.Http.HttpClient httpClient)
    {
    
        _httpClient = httpClient;
        _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
    }

Cały wygenerowany kod wygląda tak.

namespace Samurai.UI.Services
{
    using System = global::System;

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))")]
    public partial interface IClient
    {
        System.Net.Http.HttpClient HttpClient { get; }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest body);

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest body, System.Threading.CancellationToken cancellationToken);

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body);

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body, System.Threading.CancellationToken cancellationToken);

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Warrior>> GetAllSamuraisAsync();

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Warrior>> GetAllSamuraisAsync(System.Threading.CancellationToken cancellationToken);

    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))")]
    public partial class Client : IClient
    {
        private string _baseUrl = "";
        private System.Net.Http.HttpClient _httpClient;
        private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;

        public Client(System.Net.Http.HttpClient httpClient)
        {

            _httpClient = httpClient;
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
        }
        public System.Net.Http.HttpClient HttpClient
        {
            get { return _httpClient; }
        }

        private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
        {
            var settings = new Newtonsoft.Json.JsonSerializerSettings();
            UpdateJsonSerializerSettings(settings);
            return settings;
        }

        public string BaseUrl
        {
            get { return _baseUrl; }
            set { _baseUrl = value; }
        }

        protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }

        partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);


        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
        partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest body)
        {
            return AuthenticateAsync(body, System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public async System.Threading.Tasks.Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest body, System.Threading.CancellationToken cancellationToken)
        {
            if (body == null)
                throw new System.ArgumentNullException("body");

            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Login/authenticate");

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
                    content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
                    request_.Content = content_;
                    request_.Method = new System.Net.Http.HttpMethod("POST");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<AuthenticationResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            return objectResponse_.Object;
                        }
                        else
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body)
        {
            return RegisterAsync(body, System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public async System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body, System.Threading.CancellationToken cancellationToken)
        {
            if (body == null)
                throw new System.ArgumentNullException("body");

            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Login/register");

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
                    content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
                    request_.Content = content_;
                    request_.Method = new System.Net.Http.HttpMethod("POST");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<RegistrationResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            return objectResponse_.Object;
                        }
                        else
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Warrior>> GetAllSamuraisAsync()
        {
            return GetAllSamuraisAsync(System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Warrior>> GetAllSamuraisAsync(System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Samurai");

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("GET");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<System.Collections.Generic.ICollection<Warrior>>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            return objectResponse_.Object;
                        }
                        else
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        protected struct ObjectResponseResult<T>
        {
            public ObjectResponseResult(T responseObject, string responseText)
            {
                this.Object = responseObject;
                this.Text = responseText;
            }

            public T Object { get; }

            public string Text { get; }
        }

        public bool ReadResponseAsString { get; set; }

        protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
        {
            if (response == null || response.Content == null)
            {
                return new ObjectResponseResult<T>(default(T), string.Empty);
            }

            if (ReadResponseAsString)
            {
                var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                try
                {
                    var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
                    return new ObjectResponseResult<T>(typedBody, responseText);
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
                }
            }
            else
            {
                try
                {
                    using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                    using (var streamReader = new System.IO.StreamReader(responseStream))
                    using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
                    {
                        var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
                        var typedBody = serializer.Deserialize<T>(jsonTextReader);
                        return new ObjectResponseResult<T>(typedBody, string.Empty);
                    }
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
                }
            }
        }

        private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if (value == null)
            {
                return "";
            }

            if (value is System.Enum)
            {
                var name = System.Enum.GetName(value.GetType(), value);
                if (name != null)
                {
                    var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
                    if (field != null)
                    {
                        var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute))
                            as System.Runtime.Serialization.EnumMemberAttribute;
                        if (attribute != null)
                        {
                            return attribute.Value != null ? attribute.Value : name;
                        }
                    }

                    var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
                    return converted == null ? string.Empty : converted;
                }
            }
            else if (value is bool)
            {
                return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
            }
            else if (value is byte[])
            {
                return System.Convert.ToBase64String((byte[])value);
            }
            else if (value.GetType().IsArray)
            {
                var array = System.Linq.Enumerable.OfType<object>((System.Array)value);
                return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
            }

            var result = System.Convert.ToString(value, cultureInfo);
            return result == null ? "" : result;
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.11.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class AuthenticationRequest
    {
        [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Email { get; set; }

        [Newtonsoft.Json.JsonProperty("password", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Password { get; set; }


    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.11.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class AuthenticationResponse
    {
        [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Id { get; set; }

        [Newtonsoft.Json.JsonProperty("userName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string UserName { get; set; }

        [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Email { get; set; }

        [Newtonsoft.Json.JsonProperty("token", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Token { get; set; }


    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.11.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class RegistrationRequest
    {
        [Newtonsoft.Json.JsonProperty("firstName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string FirstName { get; set; }

        [Newtonsoft.Json.JsonProperty("lastName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string LastName { get; set; }

        [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Email { get; set; }

        [Newtonsoft.Json.JsonProperty("userName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string UserName { get; set; }

        [Newtonsoft.Json.JsonProperty("password", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Password { get; set; }


    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.11.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class RegistrationResponse
    {
        [Newtonsoft.Json.JsonProperty("userId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string UserId { get; set; }


    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.3.11.0 (Newtonsoft.Json v12.0.0.0)")]
    public partial class Warrior
    {
        [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Name { get; set; }

        [Newtonsoft.Json.JsonProperty("age", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int Age { get; set; }


    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))")]
    public partial class ApiException : System.Exception
    {
        public int StatusCode { get; private set; }

        public string Response { get; private set; }

        public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException)
            : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException)
        {
            StatusCode = statusCode;
            Response = response;
            Headers = headers;
        }

        public override string ToString()
        {
            return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))")]
    public partial class ApiException<TResult> : ApiException
    {
        public TResult Result { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException)
            : base(message, statusCode, response, headers, innerException)
        {
            Result = result;
        }
    }

}

W projekcie do pobranie zostawiłem też plik konfiguracyjny dla programu NSwag.

wygenerowany kod w Visual Studio 2022

Tyle, jeśli chodzi o wywołania naszego REST API. Teraz jak będziemy przesyłać JSON WEB Tokeny do naszego REST API?

Jak będziemy dodawać tokeny do zapytania ?

Najpierw stworze sobie serwis, którego zadaniem będzie dodać do instancji wygenerowanego IClient i wewnątrz niego HTTPClient token autoryzujący. 

public interface IAddBearerTokenService
{
    Task AddBearerToken(IClient client);
}

Jego implementacja wygląda tak.

public class AddBearerTokenService : IAddBearerTokenService
{
    private readonly ILocalStorageService _localStorage;


    public AddBearerTokenService(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    public async Task AddBearerToken(IClient client)
    {
        if (await _localStorage.ContainKeyAsync("token"))
            client.HttpClient.DefaultRequestHeaders.Authorization
                = new AuthenticationHeaderValue("Bearer", await _localStorage.GetItemAsync<string>("token"));
    }
}

Skąd wzięło się "ILocalStorageService". Otóż ten interfejs pochodzi z paczki NuGet : "Blazored.LocalStorage".

Paczka NuGet Blazored.LocalStorage

Blazored.LocalStorage pozwoli nam przechowywać różne dane w trakcie działania aplikacji Blazor w tym właśnie nasz token autoryzujący. Ten token zostanie zapisany w przeglądarce i gdy użytkownik uruchomi naszą aplikację Blazor ponownie to nadal będzie on zalogowany.

Aby użytkownik mógł się wylogować to musi ten token skasować ze swojego lokalnego zapisu w przeglądarce.

Co to jest LocalStorage ?
Zapisane w localStorage obiekty służą do długotrwałego przechowywania danych i pozostają na dysku po zamknięciu przeglądarki.

W przeciwieństwie do cookies, obiekty z localStorage nie są automatycznie odczytywane przez serwer przy połączeniu

Firefox, Chrome i Opera rezerwują 10 MB na domenę, a Internet Explorer 100 MB na wszystkie przechowywane dane, przy czym wartość te można zmienić na maksymalnie 20 GB. Dane przechowywane są jako tablice asocjacyjne, których kluczem i wartością jest łańcuch znaków. 

LocalStorage pojawił się wraz ze standardem HTML 5.


Pamiętaj, że każda aplikacja internetowa pisana JavaScript także ma dostęp do "localStorage"

Teraz stworzę serwis, który będzie wyciągał moich samurajów wojowników.

public interface ISamuraiService
{
    Task<List<WarriorViewModel>> GetAllWarriors();
}

Jego implementacja wygląda tak

public class SamuraiService : ISamuraiService
{
    private readonly IMapper _mapper;
    private IClient _client;
    private IAddBearerTokenService _addBearerTokenService;

    public SamuraiService(IClient client, IMapper mapper,
        IAddBearerTokenService addBearerTokenService)
    {
        _mapper = mapper;
        _client = client;
        _addBearerTokenService = addBearerTokenService;
    }

    public async Task<List<WarriorViewModel>> GetAllWarriors()
    {
        await _addBearerTokenService.AddBearerToken(_client);

        try
        {
            var allPosts = await _client.GetAllSamuraisAsync();
            var mappedPosts = _mapper.Map<ICollection<WarriorViewModel>>(allPosts);
            return mappedPosts.ToList();
        }
        catch (ApiException ex)
        {
            return new List<WarriorViewModel>();
        }
    }
}

Jak widzisz przed wywołaniem odpowiedniej metody z wygenerowanego kodu IClient spróbuje dodać wcześniej token autoryzujący. Jak pamiętasz mój serwis "IAddBearerTokenService" doda ten token do instancji HTTPClient tak, aby mógł on zostać wysłany wraz z zapytaniem HTTP. 

Jak będziemy się logować i pobierać ten token ?

A jak ten token pobierać ? To za chwilę, bo mamy ważniejsze pytanie.

Jak w Blazor przedstawić użytkownika, aby można było skorzystać z pewnych specjalnych opcji i ról w wewnątrz tej aplikacji. Nie chcemy takie weryfikacji od zera.  Zobaczmy co Blazor nam oferuje.

Do tej zabawy musimy stworzyć swoją implemtnację klasy abstrakcyjnej "AuthenticationStateProvider". Wygląda ona tak:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly ILocalStorageService _localStorage;

    public CustomAuthenticationStateProvider(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var savedToken = await _localStorage.GetItemAsync<string>("token");

        if (string.IsNullOrWhiteSpace(savedToken))
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }

        return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseTokenClaims(savedToken), "jwt")));
    }

    public void SetUserAuthenticated(string email)
    {
        var authUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
        var authState = Task.FromResult(new AuthenticationState(authUser));
        NotifyAuthenticationStateChanged(authState);
    }

    public void SetUserLoggedOut()
    {
        var anonUser = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonUser));
        NotifyAuthenticationStateChanged(authState);
    }

    private IEnumerable<Claim> ParseTokenClaims(string jwt)
    {
        var claims = new List<Claim>();
        var payload = jwt.Split('.')[1];
        var jsonBytes = ParseBase64WithoutPadding(payload);
        var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

        keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);

        if (roles != null)
        {
            if (roles.ToString().Trim().StartsWith("["))
            {
                var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());

                foreach (var parsedRole in parsedRoles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, parsedRole));
                }
            }
            else
            {
                claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
            }

            keyValuePairs.Remove(ClaimTypes.Role);
        }

        claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));

        return claims;
    }

    private byte[] ParseBase64WithoutPadding(string base64)
    {
        switch (base64.Length % 4)
        {
            case 2: base64 += "=="; break;
            case 3: base64 += "="; break;
        }
        return Convert.FromBase64String(base64);
    }
}

W metodzie przeciążonej "GetAuthenticationStateAsync" zwracam stan zalogowanego użytkownika wraz z jego rolami.

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var savedToken = await _localStorage.GetItemAsync<string>("token");

    if (string.IsNullOrWhiteSpace(savedToken))
    {
        return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }

    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseTokenClaims(savedToken), "jwt")));
}

Pierwsza metoda pozwoli nam ustawić użytkownika w aplikacji Blazor, gdy go zweryfikujemy.

public void SetUserAuthenticated(string email)
{
    var authUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
    var authState = Task.FromResult(new AuthenticationState(authUser));
    NotifyAuthenticationStateChanged(authState);
}

public void SetUserLoggedOut()
{
    var anonUser = new ClaimsPrincipal(new ClaimsIdentity());
    var authState = Task.FromResult(new AuthenticationState(anonUser));
    NotifyAuthenticationStateChanged(authState);
}

Z drugiej metody skorzystamy, gdy będziemy chcieli użytkownika wylogować. Informując przy tym aplikację Blazor, że status się zmienił poprzez wywołanie metody NotifyAuthenticationStateChanged

A jak wygląda parsowanie JSON WEB Tokenu. Token jest zapisany specyficznie w Base64. Potem gdy uzyskam z tego tablice bajtów to mogę to z deserializować na słownik klucz / wartość.

private IEnumerable<Claim> ParseTokenClaims(string jwt)
{
    var claims = new List<Claim>();
    var payload = jwt.Split('.')[1];
    var jsonBytes = ParseBase64WithoutPadding(payload);
    var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

    keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);

    if (roles != null)
    {
        if (roles.ToString().Trim().StartsWith("["))
        {
            var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());

            foreach (var parsedRole in parsedRoles)
            {
                claims.Add(new Claim(ClaimTypes.Role, parsedRole));
            }
        }
        else
        {
            claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
        }

        keyValuePairs.Remove(ClaimTypes.Role);
    }

    claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));

    return claims;
}

private byte[] ParseBase64WithoutPadding(string base64)
{
    switch (base64.Length % 4)
    {
        case 2: base64 += "=="; break;
        case 3: base64 += "="; break;
    }
    return Convert.FromBase64String(base64);
}

Tak też wyciągam rolę zapisane w tym JSON WEB Tokenie.

Teraz oddalam się od tej wymaganej klasy przez Blazor i tworzę swój serwis autoryzujący z takimi metodami.

public interface IBlazorAuthenticationService
{
    Task<bool> Authenticate(string email, string password);
    Task<bool> Register(string firstName, string lastName, string userName, string email, string password);
    Task Logout();
}

Oto jego implementacja.

public class BlazorAuthenticationService : IBlazorAuthenticationService
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    protected readonly ILocalStorageService _localStorage;

    protected IClient _client;

    public BlazorAuthenticationService(IClient client, ILocalStorageService localStorage,
        AuthenticationStateProvider authenticationStateProvider)
    {
        _authenticationStateProvider = authenticationStateProvider;
        _client = client;
        _localStorage = localStorage;
    }

    public async Task<bool> Authenticate(string email, string password)
    {
        try
        {
            AuthenticationRequest authenticationRequest = new AuthenticationRequest() { Email = email, Password = password };
            var authenticationResponse = await _client.AuthenticateAsync(authenticationRequest);

            if (authenticationResponse.Token != string.Empty)
            {
                await _localStorage.SetItemAsync("token", authenticationResponse.Token);
                ((CustomAuthenticationStateProvider)_authenticationStateProvider).SetUserAuthenticated(email);
                _client.HttpClient.DefaultRequestHeaders.Authorization
                    = new AuthenticationHeaderValue("bearer", authenticationResponse.Token);
                return true;
            }
            return false;
        }
        catch
        {
            return false;
        }
    }

    public async Task<bool> Register(string firstName, string lastName, string userName, string email, string password)
    {
        RegistrationRequest registrationRequest = new RegistrationRequest() { FirstName = firstName, LastName = lastName, Email = email, UserName = userName, Password = password };
        var response = await _client.RegisterAsync(registrationRequest);

        if (!string.IsNullOrEmpty(response.UserId))
        {
            return true;
        }
        return false;
    }

    public async Task Logout()
    {
        await _localStorage.RemoveItemAsync("token");
        ((CustomAuthenticationStateProvider)_authenticationStateProvider).SetUserLoggedOut();
        _client.HttpClient.DefaultRequestHeaders.Authorization = null;
    }
}

Jak wyciągamy Token z REST API ?

Korzystam z pewnej wygenerowanej metody "AuthenticateAsync" przez NSwag i potem zapisuje ten token w LocalStorage. 

public async Task<bool> Authenticate(string email, string password)
{
    try
    {
        AuthenticationRequest authenticationRequest = new AuthenticationRequest() { Email = email, Password = password };
        var authenticationResponse = await _client.AuthenticateAsync(authenticationRequest);

        if (authenticationResponse.Token != string.Empty)
        {
            await _localStorage.SetItemAsync("token", authenticationResponse.Token);
            ((CustomAuthenticationStateProvider)_authenticationStateProvider).SetUserAuthenticated(email);
            _client.HttpClient.DefaultRequestHeaders.Authorization
                = new AuthenticationHeaderValue("bearer", authenticationResponse.Token);
            return true;
        }
        return false;
    }
    catch
    {
        return false;
    }
}

Wylogowanie wygląda tak. Usuwam token z LocalStorage, a potem mój CustomAuthenticationStateProvider informuje Blazor o zmianie statusu użytkownika.

public async Task Logout()
{
    await _localStorage.RemoveItemAsync("token");
    ((CustomAuthenticationStateProvider)_authenticationStateProvider).SetUserLoggedOut();
    _client.HttpClient.DefaultRequestHeaders.Authorization = null;
}

Rejestracja to tylko wywołanie odpowiedniej wygenerowanej metody przez NSwag

public async Task<bool> Register(string firstName, string lastName, string userName, string email, string password)
{
    RegistrationRequest registrationRequest = new RegistrationRequest() { FirstName = firstName, LastName = lastName, Email = email, UserName = userName, Password = password };
    var response = await _client.RegisterAsync(registrationRequest);

    if (!string.IsNullOrEmpty(response.UserId))
    {
        return true;
    }
    return false;
}

Tak obecnie prezentuje się nasz projekt Blazor.

Nasz projekt Blazor w Visual Studio

Korzystając z kontenera wstrzykiwania zależności zarejestrujmy te wszystkie mechanizmy w naszej aplikacji Blazor.

Jak widzisz ustawiamy tutaj także adres naszego REST API. Tę aplikację do testów uruchomię po localhost i pod portem 5001.

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");

        //builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
        builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
        builder.Services.AddBlazoredLocalStorage();

        builder.Services.AddSingleton(new HttpClient
        {
            BaseAddress = new Uri("https://localhost:5001")
        });

        builder.Services.AddHttpClient<IClient, Client>
        (client => client.BaseAddress = new Uri("https://localhost:5001"));

        builder.Services.AddScoped<ISamuraiService, SamuraiService>();

        //Auth
        builder.Services.AddScoped<IBlazorAuthenticationService,
        BlazorAuthenticationService>();
        builder.Services.AddScoped<AuthenticationStateProvider,
        CustomAuthenticationStateProvider>();
        builder.Services.AddScoped<IAddBearerTokenService,
        AddBearerTokenService>();

Teraz możemy przejść do naszych strony pisanych w Razor, czyli składni, która pozwala nam mieszać kod HTML z C# 

Razor, czyli strony

Oto mojego wszystkie strony.

Strony Razor w naszej aplikacji Blazor

Razor strony logującej wygląda tak. Jak widzisz pola InputText są powiązane, z jaką właściwością "@LoginViewModel.Password". 

Samo wywołanie formularza wywoła jakąś metodę "HandleValidSubmit". Gdzie ona jest ? Jest ona tak zwanym CodeBehind.

@page "/login"

<div class="mt-5 row">
    <div class="card col-12 col-lg-6 mr-auto ml-auto p-3">
        <h3 class="card-title">Zaloguj się Samuraju</h3>
        <div class="card-body">
            <EditForm Model="@LoginViewModel" OnValidSubmit="HandleValidSubmit">
                <div class="input-group row mt-2">
                    <label class="col-12 col-md-4 p-0" for="userName">Email:</label>
                    <InputText id="userName" class="col-12 col-md-8 login-field" @bind-Value="LoginViewModel.Email"></InputText>
                </div>
                <div class="input-group row mt-2">
                    <label class="col-12 col-md-4 p-0" for="password">Password:</label>
                    <InputText type="password" id="password" class="col-12 col-md-8 login-field" @bind-Value="@LoginViewModel.Password"></InputText>
                </div>
                <div class="input-group row mt-2">
                    <div class="">
                        <button class="m-4 p-2">Zaloguj</button>
                    </div>
                </div>
            </EditForm>
        </div>
    </div>
</div>

<span class="label label-error">@Message</span>

Kod poboczny, który obsłuży logikę tej strony wygląda następująco. Jak widzisz w kodzie pobocznym nie możemy zrobić wstrzykiwania zależności poprzez konstruktor.

Na pomoc przychodzi nam atrybut "[Inject]", który to za nas zrobi.

public partial class Login
{
    public LoginBlazorVM LoginViewModel { get; set; }

    [Inject]
    public NavigationManager NavigationManager { get; set; }
    public string Message { get; set; }

    [Inject]
    private IBlazorAuthenticationService AuthenticationService { get; set; }

    public Login()
    {

    }

    protected override void OnInitialized()
    {
        LoginViewModel = new LoginBlazorVM();
    }

    protected async void HandleValidSubmit()
    {
        if (await AuthenticationService.Authenticate
            (LoginViewModel.Email, LoginViewModel.Password))
        {
            NavigationManager.NavigateTo("adminoptions");
        }
        Message = "Username/password Coś jest źle";
    }
}

Nasza metoda "HandleValidSubmit" wykona zapytanie HTTP do naszego REST API i poprosi o token autoryzujący. Jeśli wszystko poszło zgodnie z planem to zostaniemy przekierowani na stronę "adminoptions".

Kontrola ról użytkowników

W Blazor istnieje także możliwość kontroli ról użytkowników. Próbowałem napisać taki kod i przynajmniej jestem w stanie Ci zagwarantować, że działa sprawdzenie, czy użytkownik w ogóle jest zalogowany.

Mam pewne problemy ze sprawdzeniem konkretnych ról i mam nadzieje, że kiedyś znajdę przyczynę tego błędu w kodzie

public class Program
{
    public static async Task Main(string[] args)
    {

        ...
        builder.Services.AddAuthorizationCore(config =>
        {
            config.AddPolicy(Policies.IsAdmin, Policies.IsUserLogged());
            config.AddPolicy(Policies.IsUserLog, Policies.IsUserLogged());
            config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
            config.AddPolicy(Policies.IsClaim, Policies.IsClaimed());
        });

        await builder.Build().RunAsync();
    }
}

Warto stworzyć sobie taką klasę statyczną i weryfikować użytkowników w widoku Razor.

public static class Policies
{
    public const string IsAdmin = "IsAdmin";
    public const string IsUserLog = "IsUserLog";
    public const string IsUser = "IsUser";
    public const string IsClaim = "IsClaim";

    public static AuthorizationPolicy IsAdminPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()

                                               .RequireRole("adminEdu")
                                               .Build();
    }

    public static AuthorizationPolicy IsUserLogged()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()

                                               .Build();
    }

    public static AuthorizationPolicy IsClaimed()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
            .RequireClaim("MyCos", "MyValue")
            .Build();
    }

    public static AuthorizationPolicy IsUserPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
                                               .RequireRole("User")
                                               .Build();
    }
}

Wracamy do stron Razor

Teraz skorzystam z tej klasy statycznej "Policies". Oto kod Razor "NavMenu.razor"

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Samurai.UI</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="login">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Login
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="samurailist">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Listy Samurajów
            </NavLink>
        </li>
        <li class="nav-item px-3" Policy="@Policies.IsAdmin">
            <NavLink class="nav-link" href="adminoptions">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Admin
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Jak widzisz próbuje w ten sposób ukryć jeden z linku w tej pasku nawigującym. 

<li class="nav-item px-3" Policy="@Policies.IsAdmin">
    <NavLink class="nav-link" href="adminoptions">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Admin
    </NavLink>
</li>

W widoku można skorzystać z budowanych tagów w Razor, które ukryją odpowiednio zawartość strony przed nie zalogowany użytkownikiem.

Oto kod strony "AdminOptions.razor". Tutaj wyświetlę sobie zawartość całego tokenu JSON.

@page "/adminoptions"

<AuthorizeView Policy="@Policies.IsAdmin">
    <Authorized>

        <p class="back-to-catalog mt-3">
            <a class="back-link" @onclick="@NavigateToList">---- Zobacz listę samurajów ----</a>
        </p>
    </Authorized>
    <NotAuthorized>
        <p>Proszę się zalogować</p>
    </NotAuthorized>
</AuthorizeView>
<br />

<AuthorizeView Policy="@Policies.IsClaim">
    <Authorized>
        <p>@Policies.IsClaim : true</p>
    </Authorized>
    <NotAuthorized>
        <p>@Policies.IsClaim : false</p>
    </NotAuthorized>
</AuthorizeView>

Zawartość tokenu JSON
<p>
    @Data
</p>

<p class="back-to-catalog mt-3">
    <a class="back-link" @onclick="@DeleteToken">---- Skasuj token ----</a>
</p>

Kod poboczny strony wygląda tak. W trakcie ładowania strony wyświetlę sobie zawartość tokenu. Przy okazji oto prosty przykład jak w Blazor tworzyć dynamicznie kod HTML poprzez użycie klasy "MarkupString"

public partial class AdminOptions
{
    [Inject]
    public AuthenticationStateProvider _authenticationStateProvider { get; set; }

    [Inject]
    protected ILocalStorageService _localStorage { get; set; }

    public AdminOptions()
    {

    }

    [Parameter]
    public MarkupString Data { get; set; } = new MarkupString("");

    protected async override Task OnInitializedAsync()
    {
        var use = (await _authenticationStateProvider.GetAuthenticationStateAsync());

        StringBuilder sb = new StringBuilder();
        sb.AppendLine(use.User.Identity.Name);
        sb.Append("<br />");

        sb.AppendLine(" <ul> ");
        foreach (var item in use.User.Claims)
        {
            sb.Append(" <li> ");
            sb.Append(" <ol> ");
            sb.Append(" <li> ");
            sb.AppendLine(item.Type);
            sb.Append(" </li> ");
            sb.Append(" <li> ");
            sb.AppendLine(item.Value);
            sb.Append(" </li> ");
            sb.Append(" </ol> ");
            sb.Append(" </li> ");
        }
        sb.AppendLine(" </ul> ");
        sb.Append("<br /> Jest Adminiem");
        sb.AppendLine(use.User.IsInRole("Admin").ToString());
        sb.Append("<br /> Ma MyValue");
        sb.AppendLine(use.User.Claims.Any(p => p.Value == "MyValue").ToString());
        sb.Append("<br /> Ma Cos");
        sb.AppendLine(use.User.Claims.Any(p => p.Type == "MyCos").ToString());


        Data = new MarkupString(sb.ToString());
    }


    [Inject]
    public NavigationManager NavigationManager { get; set; }

    protected void DeleteToken()
    {
        _localStorage.RemoveItemAsync("token");
        NavigationManager.NavigateTo("/");
    }

    protected void NavigateToList()
    {
        NavigationManager.NavigateTo("/samurailist");
    }

}

Tak wygląda kod strony "SamuraiList.razor". Jej celem jest wyświetlenie wszystkich samurajów.

<AuthorizeView Policy="@Policies.IsAdmin">
    <Authorized>

        <h3>Samuraje</h3>

        @if (Warriors == null)
        {
            <p><em>Loading...</em></p>
        }
        else
        {
            <table>
                <thead>
                    <tr>
                        <th>Nazwa</th>
                        <th>Wiek</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var p in Warriors)
                    {
                        <tr>

                            <td class="post-title">@p.Name </td>
                            <td class="post-title">@p.Age </td>
                            <td class="post-title">@p.ImageUrl</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </Authorized>
    <NotAuthorized>
        <p>Proszę się zalogować. Samuraju</p>
    </NotAuthorized>
</AuthorizeView>

Oto kod poboczny tej strony. Pobieram samurajów w trakcie ładowania strony i potem przekazuje to, co pobrałem do parametru.

public partial class SamuraiList
{
    [Inject]
    public ISamuraiService SamuraiService { get; set; }

    [Inject]
    public NavigationManager NavigationManager { get; set; }

    public ICollection<WarriorViewModel> Warriors { get; set; }

    protected async override Task OnInitializedAsync()
    {
        Warriors = await SamuraiService.GetAllWarriors();
    }

}

To wszystko, zanim uruchomimy naszą aplikację Blazor warto zmienić jej port. Zakładamy, że nasze REST API będzie działać pod portem 5001. To niech nasza aplikacja Blazor działa pod portem 5005.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:50456",
      "sslPort": 44375
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
    },
    "Samurai.UI": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "dotnetRunMessages": "true",
      "applicationUrl": "https://localhost:5005;http://localhost:5006",
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
    }
  }
}

Oto chwila prawdy.

Zobaczmy jak to działa ?

Strona startowa wygląda tak:

Strona startowa Blazor

Jak widać nie możemy zobaczyć listy samurajów przed zalogowaniem więc blokady nasze działają.

Strona lista samurajów jest zablokowana ponieważ nie jesteśmy zalogowani

W panelu administratora jak widzisz nie mamy żadnych informacji.

panelu administratora jest pusty

Uruchamiamy nasze REST API, które stworzyliśmy w poprzednim wpisie.

REST API metody

Logujemy się. Mamy użytkownika eve@gmail.com z hasłem 12345

Logujemy się w naszej aplikacji Blazor

Możemy wyświetlić sobie zawartość naszego JSON WEB Tokenu.

Zawartość naszego JSON WEB Tokena

Teraz gdy jesteśmy zalogowani to możemy zobaczyć listę samurajów.

Lista Samurajów w Blazor

To wszystko. Kod można pobrać tutaj PanNiebieski/example-JsonWebToken-with-Blazor-Samurai (github.com)