JWT 验证魔术

本来今天要介绍的是验证授权,但官方那一套我不是很喜欢,所以就从网路上找其他的解决办法,果不其然就找到了一套,依照Blazor WebAssembly - JWT Authentication Example & Tutorial的思路,简单的做了一套 JWT 验证流程,让我们开始吧!


若要保留登入资料供下次使用,我们需要透过 localStorage 来达成,所以如果你的需求不是如此,便可以省略这一步。

我们先建立一个服务与其介面来处理 localStorage 相关方法:

// ILocalStorageService.cs
public interface ILocalStorageService
{
  Task<T> GetItem<T>(string key);

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

  Task RemoveItem(string key);
}
// 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

然後建立验证服务:

// IAuthService.cs
public interface IAuthService
{
  Task<bool> Check();
}
// 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 方法:

// 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 如:

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

基本的验证流程就完成了,只要有元件呼叫 IAuthService.Check 方法,就会检查 token 是否存在且有效。

这边提供测试用的 Component:

@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");
    }
}
@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


<<:  [Day26]用SEED来体验漏洞吧

>>:  28 JavaScript 的基础:AJAX 和 SetTimeout()

Day9# Function 及 Function Receiver

终於来到了第九天,今天要再更深入介绍函式(Function)以及 Function Receiver...

好想工作室 web camp JS 怎麽 training

追求更好的程序码品质 语焉不详的程序码 原本 web camp 在 training JavaSc...

Day 26:Google Map 范本学习(1)

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...

鬼故事 - CS 高手

鬼故事 - CS 高手 Credit: Vince mcmahon 灵感来源: UCCU Hacke...

Day 3 - Using the Gmail SMTP Server to Send Emails with ASP.NET Web Forms C# 使用 Gmail 做为邮件服务器来寄信

=x= 🌵 CONTACT Page 寄信页的後端寄信功能及其它注意事项。 Gmail SMTP S...