zoukankan      html  css  js  c++  java
  • Blazor client-side + webapi (.net core 3.1) 添加jwt验证流程(非host)第三章 客户端存储及验证

    准备工作:

    安装Nuget包:Blazored.LocalStorge。

    这是一个client-side 浏览器存储库,找了非常久。

    官方文档中也有一款Microsoft.AspNetCore.ProtectedBrowserStorage,具有相同功能,代码实现的方式都是通过dotnet 和 js 互操作,使用sessionStorage,官方依然不推荐使用这个包,但是却没有提供其他方式。

    安装nuget包:Microsoft.AspNetCore.Components.Authorization。

    继承并实现StatusProvider

        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");
    
                if (string.IsNullOrWhiteSpace(savedToken))
                {
                    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
                }
    
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", savedToken);
    
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
            }
    
            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);
            }
    
            private IEnumerable<Claim> ParseClaimsFromJwt(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);
            }
        }

    创建AuthService,用于在页面中使用。同时先创建IAuthService接口

        public interface IAuthService
        {
            Task<LoginResult> Login(LoginModel loginModel);
            Task Logout();
            Task<RegisterResult> Register(RegisterModel registerModel);
        }

    实现:

        public class AuthService : IAuthService
        {
            private readonly HttpClient _httpClient;
            private readonly AuthenticationStateProvider _authenticationStateProvider;
            private readonly ILocalStorageService _localStorage;
    
            public AuthService(HttpClient httpClient,
                               AuthenticationStateProvider authenticationStateProvider,
                               ILocalStorageService localStorage)
            {
                _httpClient = httpClient;
                _authenticationStateProvider = authenticationStateProvider;
                _localStorage = localStorage;
            }
    
            public async Task<RegisterResult> Register(RegisterModel registerModel)
            {
                var result = await _httpClient.PostJsonAsync<RegisterResult>($"{Program.ServerUrl}/api/register", registerModel);
    
                return result;
            }
    
            public async Task<LoginResult> Login(LoginModel loginModel)
            {
                var loginAsJson = JsonSerializer.Serialize(loginModel);
                var response = await _httpClient.PostAsync($"{Program.ServerUrl}/api/Login", new StringContent(loginAsJson, Encoding.UTF8, "application/json"));
                var loginResult = JsonSerializer.Deserialize<LoginResult>(await response.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    
                if (!response.IsSuccessStatusCode)
                {
                    return loginResult;
                }
    
                await _localStorage.SetItemAsync("authToken", loginResult.Token);
                ((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.Email);
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", loginResult.Token);
    
                return loginResult;
            }
    
            public async Task Logout()
            {
                await _localStorage.RemoveItemAsync("authToken");
                ((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
                _httpClient.DefaultRequestHeaders.Authorization = null;
            }
        }
     

    最后将上面的服务都注入,由于不同模版生产的Program.cs不太一样,这里只展示我自己的Program.cs

        public class Program
        {
            public static async Task Main(string[] args)
            {
                var builder = WebAssemblyHostBuilder.CreateDefault(args);
                builder.RootComponents.Add<App>("app");
    
                //注入服务
                builder.Services.AddBlazoredLocalStorage();
                builder.Services.AddAuthorizationCore();
                builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
                builder.Services.AddScoped<IAuthService, AuthService>();
    
                await builder.Build().RunAsync();
            }
    
            public const string ServerUrl = "https://localhost:5002";
        }
     

    添加页面

    接下来添加Login.razor和Register.razor。 

    Login.razor

    @page "/login"
    @using client.Interfaces
    @using shared
    
    @inject IAuthService AuthService
    @inject NavigationManager NavigationManager 
    
    <h1>Login</h1>
    
    @if (ShowErrors)
    {
        <div class="alert alert-danger" role="alert">
            <p>@Error</p>
        </div>
    }
    
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Please enter your details</h5>
            <EditForm Model="loginModel" OnValidSubmit="HandleLogin">
                <DataAnnotationsValidator />
                <ValidationSummary />
    
                <div class="form-group">
                    <label for="email">Email address</label>
                    <InputText Id="email" Class="form-control" @bind-Value="loginModel.Email" />
                    <ValidationMessage For="@(() => loginModel.Email)" />
                </div>
                <div class="form-group">
                    <label for="password">Password</label>
                    <InputText Id="password" type="password" Class="form-control" @bind-Value="loginModel.Password" />
                    <ValidationMessage For="@(() => loginModel.Password)" />
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </EditForm>
        </div>
    </div>
    
    @code {
        [Parameter]
        public string returnUrl { get; set; }
    
        private LoginModel loginModel = new LoginModel();
        private bool ShowErrors;
        private string Error = "";
    
    
        /// <summary>
        /// 登陆点击事件
        /// </summary>
        /// <returns></returns>
        private async Task HandleLogin()
        {
            ShowErrors = false;
    
            var result = await AuthService.Login(loginModel);
    
            if (result.Successful)
            {
                if (!string.IsNullOrEmpty(returnUrl))
                {
                    NavigationManager.NavigateTo($"/{returnUrl}");
                }
                else
                {
                    NavigationManager.NavigateTo("/");
                }
            }
            else
            {
                Error = result.Error;
                ShowErrors = true;
            }
        }
    
    }

    Register.razor

    @page "/register"
    @using client.Interfaces
    @using shared
    @inject IAuthService AuthService
    @inject NavigationManager NavigationManager
    
    <h1>Register</h1>
    
    @if (ShowErrors)
    {
        <div class="alert alert-danger" role="alert">
            @foreach (var error in Errors)
            {
                <p>@error</p>
            }
        </div>
    }
    
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Please enter your details</h5>
            <EditForm Model="RegisterModel" OnValidSubmit="HandleRegistration">
                <DataAnnotationsValidator />
                <ValidationSummary />
    
                <div class="form-group">
                    <label for="email">Email address</label>
                    <InputText Id="email" class="form-control" @bind-Value="RegisterModel.Email" />
                    <ValidationMessage For="@(() => RegisterModel.Email)" />
                </div>
                <div class="form-group">
                    <label for="password">Password</label>
                    <InputText Id="password" type="password" class="form-control" @bind-Value="RegisterModel.Password" />
                    <ValidationMessage For="@(() => RegisterModel.Password)" />
                </div>
                <div class="form-group">
                    <label for="password">Confirm Password</label>
                    <InputText Id="password" type="password" class="form-control" @bind-Value="RegisterModel.ConfirmPassword" />
                    <ValidationMessage For="@(() => RegisterModel.ConfirmPassword)" />
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </EditForm>
        </div>
    </div>
    
    @code {
    
        private RegisterModel RegisterModel = new RegisterModel();
        private bool ShowErrors;
        private IEnumerable<string> Errors;
    
        private async Task HandleRegistration()
        {
            ShowErrors = false;
    
            var result = await AuthService.Register(RegisterModel);
    
            if (result.Successful)
            {
                NavigationManager.NavigateTo("/login");
            }
            else
            {
                Errors = result.Errors;
                ShowErrors = true;
            }
        }
    
    }

    完成后,我们需要创建一个AuthorizeView组件,用来显示是否登陆。AuthorizeView是一个集成组件,它会自动根据登陆状态显示不同内容,前提是我们前面实现并注入的AuthenticationStateProvider。

    它的结构类似这样:

    <AuthorizeView>
        <Authorized>
            <!--加入已登录后的内容-->
            Hello, @context.User.Identity.Name!
            <a href="LogOut">Log out</a>
        </Authorized>
        <NotAuthorized>
            <!--加入未登录的内容-->
            <a href="Register">Register</a>
            <a href="Login">Log in</a>
        </NotAuthorized>
    </AuthorizeView>

    我们将上面的内容放入Component文件夹(或者shared,这个取决于你自己),我这里取名为:LoginDisplay.razor。

    这时候你会看到错误,不存在这个TagHelper,这是因为我们还没有导入到razor中,打开_import.razor

    添加一些引用:

    @using Microsoft.AspNetCore.Components.Authorization
    @using Blazored.LocalStorage
    @using client.Services
    @using client.Providers //取决于你的项目名

    再将LoginDisplay组件放到MainLayout.razor中Microsoft.AspNetCore.Components.Authorization

    @inherits LayoutComponentBase
    
    <div class="sidebar">
        <NavMenu />
    </div>
    
    <div class="main">
        <div class="top-row px-4">
            <LoginDisplay></LoginDisplay> //放入
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>
    
        <div class="content px-4">
            @Body
        </div>
    </div>

    完成上面后,客户端的内容基本已经完成,现在可以开始测试了。

    tips:如果你在使用Blazor wasm 3.2 preview2 ,且Microsoft.AspNetCore.Components.Authorization 版本为3.1.2,那你可能会遇到跟我一样的问题,上述代码可能可能无法loading。浏览器控制台输出提示无法找到此组件。

    这时候就需要给修改一下注入。

                builder.Services.AddAuthorizationCore(options => { });

    解决方案来自:https://github.com/dotnet/aspnetcore/issues/18733

    应该是一个bug,因为当我换成3.1.0 preview4时,代码就正常能运行。

    然后运行代码即可,项目完整源码:https://github.com/simplerjiang/AuthApiAndBlazor

  • 相关阅读:
    pyqt(一)安装及配置。
    systemctl centos fedora 用法
    onedriver -1T容量,edu邮箱申请。
    linux fdisk 添加硬盘,分区,挂载,永久挂载
    ssh的配置,ssh打开密钥登陆,关闭密码登陆。
    基金分仓
    基金交易席位的制度沿袭
    券商VIP交易通道
    解密中国证券金融股份有限公司
    光大“乌龙指”24小时
  • 原文地址:https://www.cnblogs.com/Simplerjiang/p/12356450.html
Copyright © 2011-2022 走看看