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('.')[];
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 % )
{
case : base64 += "=="; break;
case : 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
Blazor client-side + webapi (.net core 3.1) 添加jwt验证流程(非host)第三章 客户端存储及验证的更多相关文章
- ASP.NET Core 6.0 添加 JWT 认证和授权
序言 本文将分别介绍 Authentication(认证) 和 Authorization(授权). 并以简单的例子在 ASP.NET Core 6.0 的 WebAPI 中分别实现这两个功能. 相关 ...
- Blazor client-side + webapi (.net core 3.1) 添加jwt验证流程(非host)第二步 添加Identity
添加Identity数据上下文 安装nuget包:Microsoft.AspNetCore.Identity.EntityFrameworkCore 创建ApplicationDbContext类 创 ...
- .net core webapi+EF Core
.net core webapi+EF Core 一.描述: EF Core必须下载.net core2.0版本 Micorsoft.EntityFrameworkCore:EF框架的核心包Micor ...
- 【Blazor】在ASP.NET Core中使用Blazor组件 - 创建一个音乐播放器
前言 Blazor正式版的发布已经有一段时间了,.NET社区的各路高手也创建了一个又一个的Blazor组件库,其中就包括了我和其他小伙伴一起参与的AntDesign组件库,于上周终于发布了第一个版本0 ...
- 007.Adding a view to an ASP.NET Core MVC app -- 【在asp.net core mvc中添加视图】
Adding a view to an ASP.NET Core MVC app 在asp.net core mvc中添加视图 2017-3-4 7 分钟阅读时长 本文内容 1.Changing vi ...
- 【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4密码授权模式,从使用场景.原理分析.自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本 ...
- ASP.NET Core Web Api之JWT刷新Token(三)
前言 如题,本节我们进入JWT最后一节内容,JWT本质上就是从身份认证服务器获取访问令牌,继而对于用户后续可访问受保护资源,但是关键问题是:访问令牌的生命周期到底设置成多久呢?见过一些使用JWT的童鞋 ...
- abp(net core)+easyui+efcore实现仓储管理系统——EasyUI之货物管理三 (二十一)
abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...
- bp(net core)+easyui+efcore实现仓储管理系统——入库管理之二(三十八)
abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...
随机推荐
- c#数字图像处理(十)图像缩放
图像几何变换(缩放.旋转)中的常用的插值算法 在图像几何变换的过程中,常用的插值方法有最邻近插值(近邻取样法).双线性内插值和三次卷积法. 最邻近插值: 这是一种最为简单的插值方法,在图像中最小的单位 ...
- 使用requests模块的网络编程
python操作网络,也就是打开一个网站,或者请求一个http接口,本篇是介绍使用request模块的使用方式. 在使用requests模块之前需要先安装,在cmd中输入:pip install re ...
- 史上最详细的二叉树、B树,看不懂怨我
今天我们要说的红黑树就是就是一棵非严格均衡的二叉树,均衡二叉树又是在二叉搜索树的基础上增加了自动维持平衡的性质,插入.搜索.删除的效率都比较高.红黑树也是实现 TreeMap 存储结构的基石. 1.二 ...
- 响应国家号召,在家撸码之React迁移记
最近这段时间新型冠状病毒肆虐,上海确诊人数每天都在增加,人人提心吊胆,街上都没人了.为了响应国家号召,近期呆在家里撸码,着手将项目迁移到React中,项目比较朴素,是一张线索提交页面,包含表单.图片滚 ...
- mac电脑下使用fcrackzip破解zip压缩文件密码
fcrackzip简介 fcrackzip是一款专门破解zip类型压缩文件密码的工具,工具小巧方便.破解速度快,能使用字典和指定字符集破解,适用于linux.mac osx 系统 fcrackzip安 ...
- java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
问题描述 今天在使用SpringBoot整合spring security,使用内存用户验证,但无响应报错:java.lang.IllegalArgumentException: There is n ...
- OGG主从表结构不同步,出现OGG-01296错误
一.Cause ogg的err日志出现以下报错 2019-09-10 16:36:55 WARNING OGG-01003 Oracle GoldenGate Delivery for Oracle, ...
- 红帽(RedHat8) RHEL8.0系统安装教程(小白都会)
可以去了解一下Red Hat8产品详情:https://www.RedHat.com/zh/enterprise-linux-8 先准备VMware Workstation 15 Pro版本,Red ...
- js笔记(2)--第一天记录
---恢复内容开始--- 模仿了网站的一个常见小功能,开关灯小功能. 代码: <!DOCTYPE html> <html lang="en"> <he ...
- 每日一练_PAT_B_PRAC_1004客似云来
题目描述 NowCoder开了一家早餐店,这家店的客人都有个奇怪的癖好:他们只要来这家店吃过一次早餐,就会每天都过来:并且,所有人在这家店吃了两天早餐后,接下来每天都会带一位新朋友一起来品尝.于是,这 ...