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)+ ...
随机推荐
- python多层数组合成一个数组后循环打印出数组内的每一项元素的方法
a=[1,2,3,4] b=[5,6,7,8] c=[a,b] def test(c): for i in c: return i print(test(c)) 以上代码执行后打印出来的结果是 预期目 ...
- postman发送请求的简单操作
发送请求常用的是post和get 两者的区别是: 1.post比get安全,因为参数直接暴露在url中,不能用来传递敏感信息 2.get参数通过url传递,post放在request body中 3. ...
- 实现一个简易的RPC
之前写了一些关于RPC原理的文章,但是觉得还得要实现一个.之前看到一句话觉得非常有道理,与大家共勉.不是“不要重复造轮子”,而是“不要发明轮子”,所以能造轮子还是需要造的. 如果大家还有不了解原理的, ...
- 《快乐编程大本营》java语言训练班 2课:java的变量
<快乐编程大本营>java语言训练班 2课:java的变量 1变量介绍 2变量分类,数值变量 3变量分类-字符串变量 4变量分类-布尔变量 5变量分类-对象 http://code6g.c ...
- 实验20:IPv6
实验17-1: IPv6 静态路由 Ø 实验目的通过本实验可以掌握(1)启用IPv6 流量转发(2)配置IPv6 地址(3)IPv6 静态路由配置和调试(4)IPv6 默认路由配置和调试 Ø ...
- MPlayer参数使用介绍(部分)(中文)
文件太长,就不全部转载了,以下是某MPlayer网站的全部详细介绍: http://www.mplayer.org.cn/help_zh.html#%E7%A8%8B%E5%BA%8F%E9%94%9 ...
- 时序数据库 Apache-IoTDB 源码解析之文件索引块(五)
上一章聊到 TsFile 的文件组成,以及数据块的详细介绍.详情请见: 时序数据库 Apache-IoTDB 源码解析之文件数据块(四) 打一波广告,欢迎大家访问IoTDB 仓库,求一波 Star. ...
- Codeforces_723
A.取中间那个点即可. #include<bits/stdc++.h> using namespace std; ]; int main() { ios::sync_with_stdio( ...
- HDU_3853_概率dp
http://acm.hdu.edu.cn/showproblem.php?pid=3853 又因为总期望为子期望的加权和,加权因子为子期望的转移概率,所以得到:dp[ i ][ j ]= p1 * ...
- AppBox实战: 如何实现一对多表单的增删改查
本篇通过完整示例介绍如何实现一对多关系表单的相应服务及视图. 一.准备数据结构 示例所采用的数据结构为"物资需求"一对多"物资清单",通过IDE的实体设 ...