JWT 驗證機制原理與實作範例 (C#)


March 6, 2023(最後更新: June 21, 2023) 程式語言

JWT 驗證機制原理與實作範例 (C#)


JWT(JSON Web Token)是一種開放標準,用於在各方之間安全地傳遞資訊。JWT使用JSON物件表示要傳遞的訊息,並使用數字簽名或加密來保護這些訊息。

平常用於快速建立輕型網站的登入驗證,紀錄一下以 C# 實作 JWT 用法。

介紹

🔗

JWT

Encoded

由三部分組成,分別是 Header、Payload 和 Signature。
直接使用官網的範例:

Header 部分包含了JWT使用的加密算法、類型等數據。

JSON
{
  "alg": "HS256",
  "typ": "JWT"
}

Header是一個JSON物件,包含了以下兩個屬性:

  • alg:表示使用的加密算法,例如HS256、RS256等。
  • typ:表示類型,通常設置為JWT。

Payload 部分包含了要傳輸的信息。

Payload也是一個JSON物件,可以包含自定義的屬性。為了方便查詢而夾帶的一些客戶基本資料等...就是放在Payload的部分。

JSON
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Payload 中可以包含的標準屬性包括:

  • iss:JWT的發行者,表示 JWT 是由誰發行的。
  • sub:JWT的主題,表示 JWT 所代表的實體(通常是使用者)。
  • aud:JWT的接收者,表示 JWT 可以被誰使用。
  • exp:JWT的過期時間,表示 JWT 何時過期,過期的 JWT 不能再使用。
  • nbf:JWT的生效時間,表示 JWT 何時生效。
  • iat:JWT的發行時間,表示 JWT 何時被發行。
  • jti:JWT的唯一標識符,用於防止 JWT 被重複使用。

以上列出的屬性是 JWT 規格中定義的一些標準,當然也可以自定義一些名稱,用於表示應用中的特定資訊。

Signature 部分則是對Header和Payload進行數字簽名的結果。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
'your-256-bit-secret'
)

header 跟 payload 中間用.來串接,your-256-bit-secret是存放在伺服器端的自定義字串(私鑰),最後將這三個部分串接在一起後的字串進行加密演算法進行加密簽名。

簽名可以保證JWT沒有被篡改。簽名的計算方式根據不同的加密算法而不同,常見的算法包括HMAC和RSA。

原理

🔗

簡單來講就是用Header和Payload生成一個字串,字串之間以.連接,然後使用一個私密的金鑰對此字串進行簽名,最終生成一個包含Header、Payload和簽名的字符串(JWT)。 在驗證JWT時,接收方可以將Header和Payload解碼,然後使用相同的金鑰對簽名進行驗證,以確保JWT沒有被篡改。如果驗證成功,則可以信任JWT中包含的訊息。

建立JWT 範例 (.NET 6)

🔗
C#
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

// 設定 JWT 的簽名金鑰
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecretKey"));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
            new Claim(JwtRegisteredClaimNames.Sub, "user123"), // 設定使用者名稱
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // 設定 JWT ID
        };

// 設定 JWT 的有效期限為 1 小時
var expires = DateTime.UtcNow.AddHours(1);

// 建立 JWT 物件
var token = new JwtSecurityToken(
    issuer: "MyApp",
    audience: "MyClient",
    claims: claims,
    expires: expires,
    signingCredentials: signingCredentials);

// 把物件轉成 JWT 字串
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

//印出 JWT 字串
Console.WriteLine(tokenString);

驗證JWT 範例 (.NET 6)

🔗
C#
// 取得從前端攜帶的 JWT 
string tokenString = "{insert JWT token string here}";

// 設定 JWT 的驗證參數
var validationParameters = new TokenValidationParameters
{
    // 設定 JWT 的發行者和接收者
    ValidIssuer = "MyApp",
    ValidAudience = "MyClient",
    // 設定驗證 JWT 的簽名金鑰和加密算法
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecretKey")),
    ValidateIssuerSigningKey = true,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
};

try
{
    // 驗證 JWT
    var jwtHandler = new JwtSecurityTokenHandler();
    var principal = jwtHandler.ValidateToken(tokenString, validationParameters, out var validatedToken);
    var jwtToken = validatedToken as JwtSecurityToken;

    // 取得使用者名稱和 JWTID
    var username = jwtToken.Claims.First(x => x.Type == JwtRegisteredClaimNames.Sub).Value;
    var jwtId = jwtToken.Claims.First(x => x.Type == JwtRegisteredClaimNames.Jti).Value;

    // 在這裡可以執行額外的驗證,例如檢查使用者是否存在於資料庫中,以及檢查使用者是否有權限存取資源等等

    // 驗證成功
    Console.WriteLine($"Token validated. Username: {username}, JWT ID: {jwtId}");
}
catch (SecurityTokenException e)
{
    // 驗證失敗
    Console.WriteLine($"Token validation failed: {e.Message}");
}

JWT 的發行者和接收者

  • 發行者(issuer):用於表示 JWT 是由誰發行的,通常是一個識別 JWT 的組織或應用程式的名稱或網域。當我們驗證 JWT 的時候,可以檢查 JWT 的發行者是否符合預期的值,以確保 JWT 是由正確的組織或應用程式發行的。

  • 接收者(audience):用於表示 JWT 可以被誰使用,通常是一個識別 JWT 使用者的應用程式或 API 的名稱或網域。當我們驗證 JWT 的時候,可以檢查 JWT 的接收者是否符合預期的值,以確保 JWT 可以被正確的應用程式或 API 使用。


自己動手寫一個

🔗

以上是直接使用.NET Core的庫來實作 JWT 相關功能,最近接到一個需求為,需要生成一次性的檔案下載網址來防止檔案網址外流,思考後打算用 JWT 來實作。

思考邏輯為,若需要產生一次性網址,為了不產生資料庫負擔,那就產生一個有時效性的 JWT,而在提供檔案下載的 API 中再去額外加上 JWT 驗證,若時效已過則不提供下載,以達成一次性下載網址。

此功能的流程即是: 使用檔案的ID(GUID)產生 JWT ➜ 生成包含 JWT 的一次性網址 ➜ 透過網址下載時需驗證 JWT 時效性 ➜ 判斷是否給予下載

先建立 Header 與 Payload 的物件

C#
public class oJWT_Header
    {
        public string alg { get; set; }
        public string typ { get; set; }
    }
C#
public class oJWT_Payload
    {
        public long exp { get; set; }
        public long iat { get; set; }
        public Guid id { get; set; }
    }

Payload的部分,我主要需要 JWT 的過期時間(exp) 與檔案的唯一值ID(id)。

生成 JWT

🔗
C#
public static string JWT_Encoder(Guid fileID)
    {
        oJWT_Header header = new oJWT_Header();
        header.alg = "HS256";
        header.typ = "JWT";

        oJWT_Payload payload = new oJWT_Payload();
        DateTimeOffset now = DateTimeOffset.UtcNow;
        long unixTimestamp = now.ToUnixTimeSeconds();
        long unixTimestamp_exp = now.AddSeconds(20).ToUnixTimeSeconds();
        payload.iat = unixTimestamp;
        payload.exp = unixTimestamp_exp;
        payload.id = fileID;

        string json_header = JsonConvert.SerializeObject(header);
        byte[] b_header = System.Text.UTF8Encoding.UTF8.GetBytes(json_header);
        string b64_header = Convert.ToBase64String(b_header);

        string json_payload = JsonConvert.SerializeObject(payload);
        byte[] b_pl = System.Text.UTF8Encoding.UTF8.GetBytes(json_payload);
        string b64_payload = Convert.ToBase64String(b_pl);

        string encryptSignature = ComputeHMACSHA256(b64_header + b64_payload, b64_payload + payload.id);
        string token = b64_header + "." + b64_payload + "." + encryptSignature;
        
        return token;
    }

Header 物件代入分別代表使用的演算法與token形式 (alg、typ)。

Payload 物件需要代入 JWT 的發行時間(iat)、有效時間(exp)與檔案ID(id),時間都是以UTC時間轉換為Unix time。而有效時間為當下的時間再加20秒。

接下來就是對 Header 與 Payload 分別進行 Base64 的編碼。

再把編碼後的 Header 與 Payload 進行 HMACSHA256 加密,而私鑰我選擇使用 編碼後的 Payload + fileID,這樣可以確保每組網址的私鑰都是不同的也不用寫死在server端。

HMACSHA256 加密

🔗
C#
public static string ComputeHMACSHA256(string data, string key)
    {
        var keyBytes = Encoding.UTF8.GetBytes(key);
        using (var hmacSHA = new HMACSHA256(keyBytes))
        {
            var dataBytes = Encoding.UTF8.GetBytes(data);
            var hash = hmacSHA.ComputeHash(dataBytes, 0, dataBytes.Length);
            return BitConverter.ToString(hash).Replace("-", "").ToUpper();
        }
    }

HMACSHA256 加密的部分此篇不多作說明,而取得加密簽章後,JWT 即等於 : 編碼後的header.編碼後的payload.加密簽章

驗證 JWT

🔗
C#
public static bool JWT_Decoder(string JWT)
    {
        string[] ary = JWT.Split('.');
        if (ary.Length != 3) return false;
        else
        {
            string b64_header = ary[0];
            string b64_payload = ary[1];
            string Signature = ary[2];

            Byte[] ary_header = Convert.FromBase64String(b64_header);
            string json_header = System.Text.UTF8Encoding.UTF8.GetString(ary_header);
            oJWT_Header header = JsonConvert.DeserializeObject<oJWT_Header>(json_header);

            Byte[] ary_payload = Convert.FromBase64String(b64_payload);
            string json_pl = System.Text.UTF8Encoding.UTF8.GetString(ary_payload);
            oJWT_Payload payload = JsonConvert.DeserializeObject<oJWT_Payload>(json_pl);

            string encryptSignature = ComputeHMACSHA256(b64_header + b64_payload, b64_payload + payload.id);

            if (!Signature.Equals(encryptSignature)) return false;

            long exp = payload.exp;
            long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (now > exp) return false;
        }
        return true;
    }

驗證的部分很簡單,一開始因為 JWT 格式就是以.分割,所以我先以.劃分,若字符串數組長度不等於3,那不符合格式回傳 false。

接下來數組可以分為 b64_header、 b64_payload 與 Signature,分別對經過 Base64 編碼後的 Header 與 Payload 解碼,取得相關資訊後,就可以驗證 Signature 是否符合。若不符合代表JWT有被串改過即回傳 false。

最後一步,就是驗證時效性,因為當初設定時效為產生 JWT 之後20秒內,所以只要取 Payload 的 exp 來比較當下的時戳,若已超出即返回 false。

以上完成了使用 JWT 產生一次性網址的功能,實際使用 Payload 還能代入其他參數方便 下載檔案的 API 查詢檔案路徑。

總結

🔗

目前開發上只用在輕型網站的登入系統驗證機制,但其實JWT還有其他的用途像是資源權限授權、單點登錄(SSO)、時效性服務、應用程序間通訊等。藉由派發有時效性的 JWT 並在每次存取資源時驗證可以使其應用更廣泛。

CsharpWeb



Avatar

Alvin

軟體工程師,喜歡金融知識、健康觀念、心理哲學、自助旅遊與系統設計。

相關文章






留言區 (0)



  or   

尚無留言