本來今天要介紹的是驗證授權,但官方那一套我不是很喜歡,所以就從網路上找其他的解決辦法,果不其然就找到了一套,依照Blazor WebAssembly - JWT Authentication Example & Tutorial的思路,簡單的做了一套 JWT 驗證流程,讓我們開始吧!


若要保留登入資料供下次使用,我們需要透過 localStorage 來達成,所以如果你的需求不是如此,便可以省略這一步。

我們先建立一個服務與其介面來處理 localStorage 相關方法:

1
2
3
4
5
6
7
8
9
// ILocalStorageService.cs
public interface ILocalStorageService
{
Task<T> GetItem<T>(string key);

Task SetItem<T>(string key, T value);

Task RemoveItem(string key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// LocalStorageService.cs
public class LocalStorageService : ILocalStorageService
{
private readonly IJSRuntime _jsRuntime;

public LocalStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}

public async Task<T> GetItem<T>(string key)
{
var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);

if (json == null)
return default;

return JsonSerializer.Deserialize<T>(json);
}

public async Task SetItem<T>(string key, T value)
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
}

public async Task RemoveItem(string key)
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
}
}

因為 windows 物件本身就包含 localStorage 的處理,所以不需要特別撰寫 javascript

然後建立驗證服務:

1
2
3
4
5
// IAuthService.cs
public interface IAuthService
{
Task<bool> Check();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// AuthService.cs
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorageService;

public AuthService(
HttpClient httpClient,
ILocalStorageService localStorageService
)
{
_httpClient = httpClient;
_localStorageService = localStorageService;
}

public async Task<bool> Check()
{
var request = new HttpRequestMessage(HttpMethod.Get, "/auth");

var token = await _localStorageService.GetItem<string>("token");

if (token is null)
{
return false;
}

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

using var response = await _httpClient.SendAsync(request);

if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return false;
}

return true;
}
}

執行此驗證方法,會從 localStorage 取得 token 並添加到 header 中然後透過 HTTP 方法 get 呼叫 /auth 的 api,若 localStorage 中找不到 token 或者 HTTP 回應的狀態碼為 401 則回傳 false,反之為 true

但此時並沒有串接任何一個 API,所以需要建立一個假的 HttpClientHandler 來做測試,透過繼承 HttpClientHandler,並覆寫其 SendAsync 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// FakeHttpClientHandler.cs
public class FakeHttpClientHandler : HttpClientHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri.AbsolutePath == "/auth" && request.Method == HttpMethod.Get)
{
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK
};
}
else
{
return await base.SendAsync(request, cancellationToken);
}
}
}

這樣一來只要呼叫 HttpClient.SendAsync 時 HTTP 方法為 get 且位置為 /auth,就會無條件返回成功。

最後記得將上面的服務註冊到 DI Container 如:

1
2
3
builder.Services.AddScoped(sp => new HttpClient(new FakeHttpClientHandler()) { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) })
.AddScoped<ILocalStorageService, LocalStorageService>()
.AddScoped<IAuthService, AuthService>();

基本的驗證流程就完成了,只要有元件呼叫 IAuthService.Check 方法,就會檢查 token 是否存在且有效。

這邊提供測試用的 Component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@page "/day26sample"
@inject ILocalStorageService LocalStorageService;
<h3>Day26Sample</h3>
<button @onclick="SetUser">Set Token</button>
<button @onclick="RemoveUser">Remove Token</button>
<NavLink href="/day26authsample">Go Auth</NavLink>
@code {

}

@functions{
private async Task SetUser()
{
await LocalStorageService.SetItem<string>("token", @"blazor30days");
}


private async Task RemoveUser()
{
await LocalStorageService.RemoveItem("token");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@page "/day26authsample"
@inject IAuthService AuthService
@inject NavigationManager Navigation
<h3>Day26AuthSample</h3>
<NavLink href="/day26sample">Go Back</NavLink>

@code {
protected override async Task OnInitializedAsync()
{
if (!await AuthService.Check())
{
Navigation.NavigateTo("/day26sample");
}

await base.OnInitializedAsync();
}
}

可以試試看沒有設定 token 的情況下,是無法瀏覽 /day26authsample 頁面的,並會在 OnInitializedAsync 後導向 /day26sample 頁面,若有大量頁面需要做判斷,也可以考慮抽成一個共用驗證元件,透過繼承的方式一次修改所有需要驗證的元件。


以上就是我對於 JWT 驗證授權的處理方式,本來看到官方的範例想跳過此內容的,但又覺得介紹 blazor 不介紹驗證授權相關的技巧說不過去,所以就衍生出了這一篇,希望能幫助到大家。完整程式碼在範本程式碼 - Day26

感謝大家的閱讀,我們明天見。

參考資料
Blazor WebAssembly - JWT Authentication Example & Tutorial