准备工作:
安装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