首頁

目前文章總數:157 篇

  

最後更新:2024年 12月 07日

0040. .Net Core 使用Json Web Tokne(JWT)實現網站登入、登出功能 - 基本登入、登出

日期:2023年 06月 17日

標籤: C# Asp.NET Core Web Blazor Web JSON Web Tokens(JWT)

摘要:C# 學習筆記


應用所需:1. Visual Studio 2022
     2. .net core Web專案 (Blazor server示範)
範例檔案:連結
解決問題:使用JWT進行登入、登出的功能
應用層面:
基本介紹:本篇分為3大部分。
第一部分:Json Web Tokne介紹
第二部分:範例專案說明
第三部分:Demo展示結果






第一部分:Json Web Tokne介紹

Step 1:參考文獻

網路上也有很多相關的文章,JWT是來自於(RFC)請求意見稿的7519標準,有興趣的可以詳閱內容,這邊簡要說明優缺點
RFC7519

Step 2:優點


1. 使用簡單 Json格式資料
2. 輕量 每次產生的Token約落在100Byte左右
3. 跨平台交互 Json的關係,所以可以視為字串達到跨平台交互功能。
4. 無需存儲Token 無需存儲Token:無狀態性,基本上不用存在Server中(特殊用法還是需要存儲)。
5. 節省效能 同個Token可在時限內重複使用,可以再Token中放用戶信息,減少對Server與Database的查詢



Step 3:缺點


1. Server驗證 一定要準備Server來進行Token的簽名、有效性,並且Server內保存金鑰、發行等資訊。
2. 無撤銷 Token產生後,無法撤銷Token,除非時限到或者Server端的簽名憑證更換。
3. 安全性問題 相同的金鑰,是可以在任何地方偽造出相同的Token,通常金鑰過於簡單,亦容易被破解。





第二部分:範例專案說明

Step 1:範例專案架構


1. 網站建構 網站程序初始化的入口點,並且註冊注入、增加Session。
2. 顯示頁面 整個網站有2個頁面 (登入、操作頁面)。
3. JWT產生器 透過JWT產生Token(令牌),並且存入用戶資訊(非敏感)到Token中。
4. 使用模型 轉接資料型態,回傳資料用。




Step 2:網站建構

主要添加JWT的注入、Session的啟用


var builder = WebApplication.CreateBuilder(args);

\\配置其他Service...

builder.Services.AddScoped<JsonWebTokenService>();// 注入 JsonWebTokenService
var app = builder.Build();

\\配置其他app configure...

app.UseSession(); // 添加 Session 中介軟體
app.Run();




Step 3:顯示頁面-登入

登入頁面


@page "/"
@using BlazorJWTLoginExample.Model
@using System.Security.Claims
@using System.IdentityModel.Tokens.Jwt
@using BlazorJWTLoginExample.Service
@using Microsoft.IdentityModel.Tokens
@using System.Text
@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager Navigation
@inject JsonWebTokenService jwtService

<h3>登入頁面</h3>

<div class="alert @AlertClass" role="alert">@AlertMessage</div>

<!-- 1. 提供輸入帳號、密碼完成登入 -->
<EditForm Model="@LoginModel" OnValidSubmit="LoginWork">
    <DataAnnotationsValidator />

    <div class="mb-3">
        <label for="username" class="form-label">Username</label>
        <InputText id="username" class="form-control" @bind-Value="LoginModel.Username" />
        <ValidationMessage For="@(() => LoginModel.Username)" />
    </div>

    <div class="mb-3">
        <label for="password" class="form-label">Password</label>
        <InputText id="password" class="form-control" @bind-Value="LoginModel.Password" />
        <ValidationMessage For="@(() => LoginModel.Password)" />
    </div>

    <button type="submit" class="btn btn-primary">Login</button>
</EditForm>


登入按鈕點擊時觸發的驗證、產生Json Web Token


@code {
    private LoginModel LoginModel { get; set; } = new LoginModel();

    private string AlertClass { get; set; }
    private string AlertMessage { get; set; }

    //2. 頁面載入時,確認是否已經登入,如果登入則跳過Token清除
    protected override async Task OnInitializedAsync()
    {     
        await base.OnInitializedAsync();

        if (string.IsNullOrEmpty(HttpContextAccessor?.HttpContext?.Session?.GetString("JWT")))
        {
            await HttpContextAccessor.HttpContext.Session.LoadAsync();
            HttpContextAccessor.HttpContext.Session.SetString("JWT", "");
            await HttpContextAccessor.HttpContext.Session.CommitAsync();
        }
    }

    //3. 登入的實作
    private async Task LoginWork()
    {
        // 3-1. 假設驗證用戶成功...        
        var user = new LoginModel { Username = LoginModel.Username};
        // 3-2. 產生用戶假資料 (通常是資料庫取得)
        var id = 334567;
        var type = "一般用戶";
        var nickName = "Little Boy";

        var token = jwtService.GenerateToken(user, id, type, nickName);

        // 3-3. 將 JWT 存儲在 Session 中
        await HttpContextAccessor.HttpContext.Session.LoadAsync();
        HttpContextAccessor.HttpContext.Session.SetString("JWT", token);
        await HttpContextAccessor.HttpContext.Session.CommitAsync();

        // 3-4. 導頁到需要登入後才能訪問的頁面
        Navigation.NavigateTo("/protected");
    }

     
}




Step 4:顯示頁面-操作頁面

操作頁面的畫面內容,分為有通過Token驗證與沒有,有的話顯示用戶資料

@page "/protected"
@using System.IdentityModel.Tokens.Jwt
@using System.Security.Claims
@using Microsoft.IdentityModel.Tokens
@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager Navigation

<!-- 1. 檢核當前是否已經登入 -->
@if (IsAuthenticated)
{
    <h3>歡迎使用本系統:@UserName</h3>
    <h3>權限:@Type</h3>
    <h3>身分證ID:@Id</h3>
    <h3>暱稱:@NickName</h3>
    <!-- 4-1. 登出按鈕-->
    <div class="nav-item px-3">
        <button @onclick="LogoutMethod">登出</button>
    </div>
}
else
{
    <p>未登入</p>
}



操作頁面實現驗證Token、存進用戶資料的方法


@code {
    //2. 登入條件:依照當前Token是否驗證通過
    private bool IsAuthenticated => ValidateJwtToken(HttpContextAccessor?.HttpContext?.Session?.GetString("JWT"));
    private string UserName { get; set; } = string.Empty;
    private string Type { get; set; } = string.Empty;
    private string Id { get; set; } = string.Empty;
    private string NickName { get; set; } = string.Empty;

    //3. 驗證Token
    private bool ValidateJwtToken(string token)
    {
        //3-1. 空的為驗證失敗
        if (string.IsNullOrEmpty(token))
            return false;

        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadJwtToken(token);

        //3-2. token存在的情況下,檢查是否過期
        if (CheckExpiration(jwtToken))
        {
            //3-3.沒有過期則將用戶資料從Token取出,顯示在操作頁面上
            UserName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
            Type = jwtToken.Claims.FirstOrDefault(c => c.Type == "type")?.Value;
            Id = jwtToken.Claims.FirstOrDefault(c => c.Type == "id")?.Value; ;
            NickName = jwtToken.Claims.FirstOrDefault(c => c.Type == "nickname")?.Value; ;
            return true; // JWT Token 仍然有效
        }
        return false;
    }

    //檢查JWT過期
    private bool CheckExpiration(JwtSecurityToken jwtToken)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var expiration = jwtToken.ValidTo; // 取得過期時間
        var now = DateTime.UtcNow; // 取得當前時間
        if (expiration < now)
        {
            return false; // JWT Token 已經過期
        }
        return true;
    }

    //4-2. 登出行為
    private void LogoutMethod()
    {
        // 移除 Session 中的 JWT,導回首頁
        HttpContextAccessor.HttpContext.Session.Remove("JWT");
        Navigation.NavigateTo("/");

    }
}




Step 5:JWT產生器


a. 帶用戶資訊 將需要傳遞的非敏感訊息放進Token中,達成Payload物件的效果。
    只要令牌沒有失效,在任何地方此Token只要有正確的簽名憑證都可以解析(發行與使用者資訊也必需正確)。
b. 簽名憑證 這邊用預設的SHA256加密金鑰,在傳遞Token為密文的形式。
c. Token內容 將簽名、有效日期、發行者、使用者、Payload等資訊產生出唯一Token。
d. 回傳 最後將Token轉為字串格式使用。

public class JsonWebTokenService
{

    public const string _issuer = "";
	//
    public const string _audience = "";
    public const string _secretKey = "this_is_a_secure_key_with_length_greater_than_32";
    /// <summary>
    /// 產生JWT 
    /// </summary>
    public string GenerateToken(LoginModel user,
        int id, string type, string nickName)
    {
        // 1.攜帶用戶資訊(塞在Token中)
        var claims = new[]
         {
         new Claim(ClaimTypes.Name, user.Username),
         new Claim("type", type),
         new Claim("id", $@"{id}"),
         new Claim("nickname", nickName),
     };
        // 2. 使用加密金鑰 與 SHA256加密算法創建簽名憑證
        var jwtKey = Encoding.UTF8.GetBytes(_secretKey);
        var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(jwtKey), SecurityAlgorithms.HmacSha256);
        // 3. 定義 JWT 的內容
        var token = new JwtSecurityToken(
            issuer: _issuer,     // 發行者:若解析驗證Token正確性時這個不同會視為驗證失敗
            audience: _audience, // 使用者:若解析驗證Token正確性時這個不同會視為驗證失敗
            signingCredentials: signingCredentials,//簽名憑證:若解析驗證Token正確性時這個不同會視為驗證失敗
            claims: claims,      // 資料:可攜帶用戶資訊,像密碼類的不建議放進,如果被收集過多的token仍有可能被破解
            expires: DateTime.UtcNow.AddSeconds(10)//過期時間:如果超過此token會直接報廢
        );
        // 4. 最後產生token為字串格式
        return  new JwtSecurityTokenHandler().WriteToken(token);             
    }
}




Step 5:使用模型


登入模型:登入驗證基本所需必要條件。


public class LoginModel
{
    /// <summary>
    /// 帳號
    /// </summary>
    public string Username { get; set; } = string.Empty;

    /// <summary>
    /// 密碼
    /// </summary>
    public string Password { get; set; } = string.Empty;
}






第三部分:Demo展示結果

Step 1:登入


我們隨意輸入帳號密碼 -> 登入


Step 2:登入成功


登入後成功取得該用戶資訊


Step 3:登出條件-1


我們在系統中設定10秒過期時間,假設超過10秒,再F5重新整理就會因為Token過期回到登入頁面

// 3. 定義 JWT 的內容
var token = new JwtSecurityToken(
    expires: DateTime.UtcNow.AddSeconds(10)//過期時間:如果超過此token會直接報廢
);




Step 4:登出條件-2


按下登出後,也會強制將Token清除。



Step 5:Token的安全性


補充:Token內不能攜帶重要的資料,Token的簽章與加密方式是有可能被破解的。
這邊將產生的Token複製,利用解析Token網站貼上Token解析後可看到內容。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c