iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Modern Web

今晚,我想來點Blazor系列 第 25

Day 25:Blazor jwt登入範例(4) -- 驗證及授權

  • 分享至 

  • xImage
  •  

上一篇可以順利取得jwt token後,這一篇來看Blazor WebAssembly要怎麼進行jwt的驗證。

安裝套件

  1. 安裝Microsoft.AspNetCore.Components.WebAssembly.Authentication,啟用驗證機制
  2. Blazored.LocalStorage。因為token會放在瀏覽器的的localstorage,所以可以安裝這個套件來存取localstorage,或是使用IJSRuntime物件,用javascript來存取也可以。

AuthenticationStateProvider

還記得這個類別嗎? 在前天的介紹中,我們提到繼承它並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);
        }
    }
  • GetAuthenticationStateAsync:檢查localstorage是否有token,沒有的話回傳anonymous user,有的話將token轉為claim,在header設定bearer token,回傳帶有claim的AuthenticationState物件。
  • NotifyUserAuthentication:第一次登入後,通知驗證相關的元件,例如AuthorizeView更新狀態。
  • NotifyUserLogOut:登出後,通知驗證相關的元件更新狀態。

接下來新增一個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:

  • 注入LocalStorageService、httpClient、AuthenticationStateProvider 物件
  • 將帳號密碼送到api做驗證,驗證成功會回傳UserToken物件
  • 將token寫到localstorage
  • 通知驗證相關元件更新狀態
  • 每次request的header帶入bearer token

LogoutAsync:

  • 移除localstorage中的token
  • 通知驗證相關元件更新狀態
  • 每次request的header設為null

登入、登出元件

我們在此系列文的第一篇,已經做好登入元件,並確定能取得輸入資料,現在我們要改寫登入的程式碼。

@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元件:

  • 帳號密碼都通過required驗證就送出表單
  • 透過authService物件做登入判斷
  • 登入成功回首頁,失敗則彈跳出登入失敗

logout元件,初始化時進行登出,返回首頁:

@page "/Logout"
@inject IAuthService authService
@inject NavigationManager navigation

@code {
    protected override async Task OnInitializedAsync()
    {
        await authService.LogoutAsync();
        navigation.NavigateTo("/");
    }
}

LoginDisplay元件

這個元件在登入前顯示的是login的連結,登入後顯示welcome文字

<AuthorizeView>
    <Authorized>
        Welcome, @context.User.Identity.Name  <a href="/Logout">Logout</a>
    </Authorized>
    <NotAuthorized>
        <a href="/Login">Login</a>
    </NotAuthorized>
</AuthorizeView>

@code {

}
  • 使用AuthirizedView,驗證通過就顯示Authorized的內容,反之顯示NotAuthorized的內容

登入前:
https://ithelp.ithome.com.tw/upload/images/20201009/20130058wKNcq6Q6ii.jpg

登入成功後:
https://ithelp.ithome.com.tw/upload/images/20201009/20130058QLou8RQcEH.jpg

程式碼可參考:https://github.com/CircleLin/BlazorLoginWithJWT


上一篇
Day 24:Blazor jwt登入範例(3) -- 產生jwt token
下一篇
Day 26:Blazor WebAssembly 上傳檔案
系列文
今晚,我想來點Blazor30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
nlpsl202
iT邦新手 5 級 ‧ 2022-01-17 02:15:58

請問Blazor server適用此作法嗎?
我試了要把AuthenticationStateProvider改成ServerAuthenticationStateProvider才可以,但不知為什麼登入成功後跑頁面都是跑NotAuthorized的區塊,不知道問題出在哪

我要留言

立即登入留言