上一篇可以順利取得jwt token後,這一篇來看Blazor WebAssembly要怎麼進行jwt的驗證。
還記得這個類別嗎? 在前天的介紹中,我們提到繼承它並override GetAuthenticationStateAsync方法後,可以讓AuthorizeView取得驗證狀態和user claim等等資料。
現在我們新增一個JwtAuthenticationStateProvider類別:
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService localStorageService;
private readonly HttpClient httpClient;
private AuthenticationState anonymous;
public JwtAuthenticationStateProvider(ILocalStorageService localStorageService, HttpClient httpClient)
{
anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
this.localStorageService = localStorageService;
this.httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
string tokenInLocalStorage = await localStorageService.GetItemAsStringAsync("authToken");
if (string.IsNullOrEmpty(tokenInLocalStorage))
{
//沒有的話,回傳匿名使用者
return anonymous;
}
var claims = JwtParser.ParseClaimsFromJwt(tokenInLocalStorage);
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", tokenInLocalStorage);
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
}
public void NotifyUserAuthentication(string token)
{
var claims = JwtParser.ParseClaimsFromJwt(token);
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void NotifyUserLogOut()
{
var authState = Task.FromResult(anonymous);
NotifyAuthenticationStateChanged(authState);
}
}
接下來新增一個AuthService,來處理登入相關邏輯
public class AuthService : IAuthService
{
private readonly ILocalStorageService localStorageService;
private readonly HttpClient httpClient;
private readonly AuthenticationStateProvider authenticationStateProvider;
public AuthService(ILocalStorageService localStorageService, HttpClient httpClient, AuthenticationStateProvider authenticationStateProvider)
{
this.localStorageService = localStorageService;
this.httpClient = httpClient;
this.authenticationStateProvider = authenticationStateProvider;
}
public async Task<bool> LoginAsync(UserInfo userInfo)
{
bool result = false;
var json = JsonConvert.SerializeObject(userInfo);
HttpContent httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync("/api/Auth/Login", httpContent);
if (response.IsSuccessStatusCode)
{
var resContent = await response.Content.ReadAsStringAsync();
UserToken userToken = JsonConvert.DeserializeObject<UserToken>(resContent);
await localStorageService.SetItemAsync<string>("authToken", userToken.token);
((JwtAuthenticationStateProvider)authenticationStateProvider).NotifyUserAuthentication(userToken.token);
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", userToken.token);
result = true;
}
return result;
}
public async Task LogoutAsync()
{
await localStorageService.RemoveItemAsync("authToken");
((JwtAuthenticationStateProvider)authenticationStateProvider).NotifyUserLogOut();
httpClient.DefaultRequestHeaders.Authorization = null;
}
}
LoginAsync:
LogoutAsync:
我們在此系列文的第一篇,已經做好登入元件,並確定能取得輸入資料,現在我們要改寫登入的程式碼。
@page "/login"
@layout LoginLayout
@inject IJSRuntime js
@inject NavigationManager navigation
@inject IAuthService authService
<div class="card">
<div class="card-body my-2">
<h3>Login</h3>
<hr />
<EditForm Model="loginModel" OnValidSubmit="SubmitHandlerAsync">
<DataAnnotationsValidator />
<div class="form-group">
<label for="email">Email</label>
<InputText @bind-Value="loginModel.Email" class="form-control" id="email" />
<ValidationMessage For="()=>loginModel.Email" />
</div>
<div class="form-group">
<label for="pw">Password</label>
<InputPassword @bind-Value="loginModel.Password" class="form-control" id="pw" />
<ValidationMessage For="()=>loginModel.Password" />
</div>
@if (IsSubmit)
{
<button class="btn btn-primary btn-block" type="button" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Loading...</span>
</button>
}
else
{
<button class="btn btn-primary btn-block">Submit</button>
}
</EditForm>
</div>
</div>
@code {
private bool IsSubmit = false;
private LoginModel loginModel = new LoginModel();
private async Task SubmitHandlerAsync()
{
//Console.WriteLine($"Email:{loginModel.Email} / Password:{loginModel.Password}");
IsSubmit = true;
UserInfo userInfo = new UserInfo()
{
Email = loginModel.Email,
Password = loginModel.Password
};
bool result = await authService.LoginAsync(userInfo);
if (result)
{
navigation.NavigateTo("/");
}
else
{
await js.InvokeVoidAsync("alert", "登入失敗");
}
IsSubmit = false;
}
}
login元件:
logout元件,初始化時進行登出,返回首頁:
@page "/Logout"
@inject IAuthService authService
@inject NavigationManager navigation
@code {
protected override async Task OnInitializedAsync()
{
await authService.LogoutAsync();
navigation.NavigateTo("/");
}
}
這個元件在登入前顯示的是login的連結,登入後顯示welcome文字
<AuthorizeView>
<Authorized>
Welcome, @context.User.Identity.Name <a href="/Logout">Logout</a>
</Authorized>
<NotAuthorized>
<a href="/Login">Login</a>
</NotAuthorized>
</AuthorizeView>
@code {
}
登入前:
登入成功後:
程式碼可參考:https://github.com/CircleLin/BlazorLoginWithJWT
請問Blazor server適用此作法嗎?
我試了要把AuthenticationStateProvider改成ServerAuthenticationStateProvider才可以,但不知為什麼登入成功後跑頁面都是跑NotAuthorized的區塊,不知道問題出在哪