分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
網路上也有很多相關的文章,JWT是來自於(RFC)請求意見稿的7519標準,有興趣的可以詳閱內容,這邊簡要說明優缺點
RFC7519
1. 使用簡單 | : | Json格式資料 |
2. 輕量 | : | 每次產生的Token約落在100Byte左右 |
3. 跨平台交互 | : | Json的關係,所以可以視為字串達到跨平台交互功能。 |
4. 無需存儲Token | : | 無需存儲Token:無狀態性,基本上不用存在Server中(特殊用法還是需要存儲)。 |
5. 節省效能 | : | 同個Token可在時限內重複使用,可以再Token中放用戶信息,減少對Server與Database的查詢 |
1. Server驗證 | : | 一定要準備Server來進行Token的簽名、有效性,並且Server內保存金鑰、發行等資訊。 |
2. 無撤銷 | : | Token產生後,無法撤銷Token,除非時限到或者Server端的簽名憑證更換。 |
3. 安全性問題 | : | 相同的金鑰,是可以在任何地方偽造出相同的Token,通常金鑰過於簡單,亦容易被破解。 |
1. 網站建構 | : | 網站程序初始化的入口點,並且註冊注入、增加Session。 |
2. 顯示頁面 | : | 整個網站有2個頁面 (登入、操作頁面)。 |
3. JWT產生器 | : | 透過JWT產生Token(令牌),並且存入用戶資訊(非敏感)到Token中。 |
4. 使用模型 | : | 轉接資料型態,回傳資料用。 |
主要添加JWT的注入、Session的啟用
var builder = WebApplication.CreateBuilder(args);
\\配置其他Service...
builder.Services.AddScoped<JsonWebTokenService>();// 注入 JsonWebTokenService
var app = builder.Build();
\\配置其他app configure...
app.UseSession(); // 添加 Session 中介軟體
app.Run();
登入頁面
@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");
}
}
操作頁面的畫面內容,分為有通過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("/");
}
}
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);
}
}
登入模型:登入驗證基本所需必要條件。
public class LoginModel
{
/// <summary>
/// 帳號
/// </summary>
public string Username { get; set; } = string.Empty;
/// <summary>
/// 密碼
/// </summary>
public string Password { get; set; } = string.Empty;
}
我們隨意輸入帳號密碼 -> 登入
登入後成功取得該用戶資訊
我們在系統中設定10秒過期時間,假設超過10秒,再F5重新整理就會因為Token過期回到登入頁面
// 3. 定義 JWT 的內容
var token = new JwtSecurityToken(
expires: DateTime.UtcNow.AddSeconds(10)//過期時間:如果超過此token會直接報廢
);
按下登出後,也會強制將Token清除。
補充:Token內不能攜帶重要的資料,Token的簽章與加密方式是有可能被破解的。
這邊將產生的Token複製,利用解析Token網站貼上Token解析後可看到內容。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c