【.NET Core項目實戰-統一認證平臺】第十二章 授權篇-深入理解JWT生成及驗證流程

来源:https://www.cnblogs.com/jackcao/archive/2018/12/29/10195351.html
-Advertisement-
Play Games

" 【.NET Core項目實戰 統一認證平臺】開篇及目錄索引 " 上篇文章介紹了基於 密碼授權模式,從使用場景、原理分析、自定義帳戶體系集成完整的介紹了密碼授權模式的內容,並最後給出了三個思考問題,本篇就針對第一個思考問題詳細的講解下 是如何生成access_token的,如何驗證access_t ...


【.NET Core項目實戰-統一認證平臺】開篇及目錄索引

上篇文章介紹了基於Ids4密碼授權模式,從使用場景、原理分析、自定義帳戶體系集成完整的介紹了密碼授權模式的內容,並最後給出了三個思考問題,本篇就針對第一個思考問題詳細的講解下Ids4是如何生成access_token的,如何驗證access_token的有效性,最後我們使用.net webapi來實現一個外部介面(本來想用JAVA來實現的,奈何沒學好,就當拋磚引玉吧,有會JAVA的朋友根據我寫的案例使用JAVA來實現一個案例)。

.netcore項目實戰交流群(637326624),有興趣的朋友可以在群里交流討論。

一、JWT簡介

  1. 什麼是JWT?
    JSON Web Token (JWT)是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,用於作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數字簽名的。

  2. 什麼時候使用JWT?

1)、認證,這是比較常見的使用場景,只要用戶登錄過一次系統,之後的請求都會包含簽名出來的token,通過token也可以用來實現單點登錄。

2)、交換信息,通過使用密鑰對來安全的傳送信息,可以知道發送者是誰、放置消息是否被篡改。

  1. JWT的結構是什麼樣的?

JSON Web Token由三部分組成,它們之間用圓點(.)連接。這三部分分別是:

  • Header
  • Payload
  • Signature

Header
header典型的由兩部分組成:token的類型(“JWT”)和演算法名稱(比如:HMAC SHA256或者RSA等等)。

例如:

{
    "alg": "RS256",
    "typ": "JWT"
}

然後,用Base64對這個JSON編碼就得到JWT的第一部分

Payload

JWT的第二部分是payload,它包含聲明(要求)。聲明是關於實體(通常是用戶)和其他數據的聲明。聲明有三種類型: registered, public 和 private。

  • Registered claims : 這裡有一組預定義的聲明,它們不是強制的,但是推薦。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
  • Public claims : 可以隨意定義。
  • Private claims : 用於在同意使用它們的各方之間共用信息,並且不是註冊的或公開的聲明。

下麵是一個例子:

{
 "nbf": 1545919058,
 "exp": 1545922658,
 "iss": "http://localhost:7777",
 "aud": [
     "http://localhost:7777/resources",
     "mpc_gateway"
 ],
 "client_id": "clienta",
 "sub": "1",
 "auth_time": 1545919058,
 "idp": "local",
 "nickname": "金焰的世界",
 "email": "[email protected]",
 "mobile": "13888888888",
 "scope": [
     "mpc_gateway",
     "offline_access"
 ],
 "amr": [
     "pwd"
 ]
}

對payload進行Base64編碼就得到JWT的第二部分

註意,不要在JWT的payload或header中放置敏感信息,除非它們是加密的。

Signature

為了得到簽名部分,你必須有編碼過的header、編碼過的payload、一個秘鑰,簽名演算法是header中指定的                那個,然對它們簽名即可。

例如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

簽名是用於驗證消息在傳遞過程中有沒有被更改,並且,對於使用私鑰簽名的token,它還可以驗證JWT的發送方是否為它所稱的發送方。

二、IdentityServer4是如何生成jwt的?

在瞭解了JWT的基本概念介紹後,我們要知道JWT是如何生成的,加密的方式是什麼,我們如何使用自己的密鑰進行加密。

IdentityServer4的加密方式?

Ids4目前使用的是RS256非對稱方式,使用私鑰進行簽名,然後客戶端通過公鑰進行驗簽。可能有的人會問,我們在生成Ids4時,也沒有配置證書,為什麼也可以運行起來呢?這裡就要講解證書的使用,以及Ids4使用證書的加密流程。

1、載入證書

Ids4預設使用臨時證書來進行token的生成,使用代碼 .AddDeveloperSigningCredential(),這裡會自動給生成tempkey.rsa證書文件,所以項目如果使用預設配置的根目錄可以查看到此文件,實現代碼如下:

public static IIdentityServerBuilder AddDeveloperSigningCredential(this IIdentityServerBuilder builder, bool persistKey = true, string filename = null)
{
    if (filename == null)
    {
        filename = Path.Combine(Directory.GetCurrentDirectory(), "tempkey.rsa");
    }

    if (File.Exists(filename))
    {
        var keyFile = File.ReadAllText(filename);
        var tempKey = JsonConvert.DeserializeObject<TemporaryRsaKey>(keyFile, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() });

        return builder.AddSigningCredential(CreateRsaSecurityKey(tempKey.Parameters, tempKey.KeyId));
    }
    else
    {
        var key = CreateRsaSecurityKey();

        RSAParameters parameters;

        if (key.Rsa != null)
            parameters = key.Rsa.ExportParameters(includePrivateParameters: true);
        else
            parameters = key.Parameters;

        var tempKey = new TemporaryRsaKey
        {
            Parameters = parameters,
            KeyId = key.KeyId
        };

        if (persistKey)
        {
            File.WriteAllText(filename, JsonConvert.SerializeObject(tempKey, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() }));
        }

        return builder.AddSigningCredential(key);
    }
}

這也就可以理解為什麼沒有配置證書也一樣可以使用了。

註意:在生產環境我們最好使用自己配置的證書。

如果我們已經有證書了,可以使用如下代碼實現,至於證書是如何生成的,網上資料很多,這裡就不介紹了。

 .AddSigningCredential(new X509Certificate2(Path.Combine(basePath,"test.pfx"),"123456"));

然後註入證書相關信息,代碼如下:

builder.Services.AddSingleton<ISigningCredentialStore>(new DefaultSigningCredentialsStore(credential));
            builder.Services.AddSingleton<IValidationKeysStore>(new DefaultValidationKeysStore(new[] { credential.Key }));

後面就可以在項目里使用證書的相關操作了,比如加密、驗簽等。

2、使用證書加密

上篇我介紹了密碼授權模式,詳細的講解了流程,當所有信息校驗通過,Claim生成完成後,就開始生成token了,核心代碼如下。

public virtual async Task<string> CreateTokenAsync(Token token)
{
    var header = await CreateHeaderAsync(token);
    var payload = await CreatePayloadAsync(token);
    return await CreateJwtAsync(new JwtSecurityToken(header, payload));
}
//使用配置的證書生成JWT頭部
protected virtual async Task<JwtHeader> CreateHeaderAsync(Token token)
{
    var credential = await Keys.GetSigningCredentialsAsync();

    if (credential == null)
    {
        throw new InvalidOperationException("No signing credential is configured. Can't create JWT token");
    }

    var header = new JwtHeader(credential);

    // emit x5t claim for backwards compatibility with v4 of MS JWT library
    if (credential.Key is X509SecurityKey x509key)
    {
        var cert = x509key.Certificate;
        if (Clock.UtcNow.UtcDateTime > cert.NotAfter)
        {//如果證書過期提示
            Logger.LogWarning("Certificate {subjectName} has expired on {expiration}", cert.Subject, cert.NotAfter.ToString(CultureInfo.InvariantCulture));
        }
        header["x5t"] = Base64Url.Encode(cert.GetCertHash());
    }

    return header;
}
//生成內容
public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, ILogger logger)
{
    var payload = new JwtPayload(
        token.Issuer,
        null,
        null,
        clock.UtcNow.UtcDateTime,
        clock.UtcNow.UtcDateTime.AddSeconds(token.Lifetime));

    foreach (var aud in token.Audiences)
    {
        payload.AddClaim(new Claim(JwtClaimTypes.Audience, aud));
    }

    var amrClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.AuthenticationMethod);
    var scopeClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.Scope);
    var jsonClaims = token.Claims.Where(x => x.ValueType == IdentityServerConstants.ClaimValueTypes.Json);

    var normalClaims = token.Claims
        .Except(amrClaims)
        .Except(jsonClaims)
        .Except(scopeClaims);

    payload.AddClaims(normalClaims);

    // scope claims
    if (!scopeClaims.IsNullOrEmpty())
    {
        var scopeValues = scopeClaims.Select(x => x.Value).ToArray();
        payload.Add(JwtClaimTypes.Scope, scopeValues);
    }

    // amr claims
    if (!amrClaims.IsNullOrEmpty())
    {
        var amrValues = amrClaims.Select(x => x.Value).Distinct().ToArray();
        payload.Add(JwtClaimTypes.AuthenticationMethod, amrValues);
    }

    // deal with json types
    // calling ToArray() to trigger JSON parsing once and so later 
    // collection identity comparisons work for the anonymous type
    try
    {
        var jsonTokens = jsonClaims.Select(x => new { x.Type, JsonValue = JRaw.Parse(x.Value) }).ToArray();

        var jsonObjects = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Object).ToArray();
        var jsonObjectGroups = jsonObjects.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonObjectGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON object and the other is not a JSON object ({0})", group.Key));
            }

            if (group.Skip(1).Any())
            {
                // add as array
                payload.Add(group.Key, group.Select(x => x.JsonValue).ToArray());
            }
            else
            {
                // add just one
                payload.Add(group.Key, group.First().JsonValue);
            }
        }

        var jsonArrays = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Array).ToArray();
        var jsonArrayGroups = jsonArrays.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonArrayGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON array and the other is not a JSON array ({0})", group.Key));
            }

            var newArr = new List<JToken>();
            foreach (var arrays in group)
            {
                var arr = (JArray)arrays.JsonValue;
                newArr.AddRange(arr);
            }

            // add just one array for the group/key/claim type
            payload.Add(group.Key, newArr.ToArray());
        }

        var unsupportedJsonTokens = jsonTokens.Except(jsonObjects).Except(jsonArrays);
        var unsupportedJsonClaimTypes = unsupportedJsonTokens.Select(x => x.Type).Distinct();
        if (unsupportedJsonClaimTypes.Any())
        {
            throw new Exception(string.Format("Unsupported JSON type for claim types: {0}", unsupportedJsonClaimTypes.Aggregate((x, y) => x + ", " + y)));
        }

        return payload;
    }
    catch (Exception ex)
    {
        logger.LogCritical(ex, "Error creating a JSON valued claim");
        throw;
    }
}
//生成最終的Token
protected virtual Task<string> CreateJwtAsync(JwtSecurityToken jwt)
{
    var handler = new JwtSecurityTokenHandler();
    return Task.FromResult(handler.WriteToken(jwt));
}

知道了這些原理後,我們就能清楚的知道access_token都放了那些東西,以及我們可以如何來驗證生成的Token

三、如何驗證access_token的有效性?

知道瞭如何生成後,最主要的目的還是要直接我們服務端是如何來保護介面安全的,為什麼服務端只要加入下代碼就能夠保護配置的資源呢?

services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
            {
               options.Authority ="http://localhost:7777";
               options.RequireHttpsMetadata = false;
               options.ApiName = "Api1";
               options.SaveToken = true;
            });
//啟用授權 
app.UseAuthentication();

在理解這個前,我們需要瞭解系統做的驗證流程,這裡使用一張圖可以很好的理解流程了。

img
看完後是不是豁然開朗?這裡就可以很好的理解/.well-known/openid-configuration/jwks原來就是證書的公鑰信息,是通過訪問/.well-known/openid-configuration暴露給所有的客戶端使用,安全性是用過非對稱加密的原理保證,私鑰加密的信息,公鑰只能驗證,所以也不存在密鑰泄漏問題。

雖然只是短短的幾句代碼,就做了那麼多事情,這說明Ids4封裝的好,減少了我們很多編碼工作。這是有人會問,那如果我們的項目不是.netcore的,那如何接入到網關呢?

網上有一個Python例子,用 Identity Server 4 (JWKS 端點和 RS256 演算法) 來保護 Python web api.

本來準備使用Java來實現,好久沒摸已經忘了怎麼寫了,留給會java的朋友實現吧,原理都是一樣。

下麵我就已webapi為例來開發服務端介面,然後使用Ids4來保護介面內容。

新建一個webapi項目,項目名稱Czar.AuthPlatform.WebApi,為了讓輸出的結果為json,我們需要在WebApiConfig增加config.Formatters.Remove(config.Formatters.XmlFormatter);代碼,然後修改預設的控制器ValuesController,修改代碼如下。

[Ids4Auth("http://localhost:6611", "mpc_gateway")]
public IEnumerable<string> Get()
{
      var Context = RequestContext.Principal; 
      return new string[] { "WebApi Values" };
}

為了保護api安全,我們需要增加一個身份驗證過濾器,實現代碼如下。

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Czar.AuthPlatform.WebApi
{
    public class Ids4AuthAttribute : AuthorizationFilterAttribute
    {
        /// <summary>
        /// 認證伺服器地址
        /// </summary>
        private string issUrl = "";
        /// <summary>
        /// 保護的API名稱
        /// </summary>
        private string apiName = "";

        public Ids4AuthAttribute(string IssUrl,string ApiName)
        {
            issUrl = IssUrl;
            apiName = ApiName;
        }
        /// <summary>
        /// 重寫驗證方式
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            try
            {
                var access_token = actionContext.Request.Headers.Authorization?.Parameter; //獲取請求的access_token
                if (String.IsNullOrEmpty(access_token))
                {//401
                    actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                    actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未授權\"}");
                }
                else
                {//開始驗證請求的Token是否合法
                    //1、獲取公鑰
                    var httpclient = new HttpClient();
                    var jwtKey= httpclient.GetStringAsync(issUrl + "/.well-known/openid-configuration/jwks").Result;
                    //可以在此處緩存jwtkey,不用每次都獲取。
                    var Ids4keys = JsonConvert.DeserializeObject<Ids4Keys>(jwtKey);
                    var jwk = Ids4keys.keys;
                    var parameters = new TokenValidationParameters
                    { //可以增加自定義的驗證項目
                        ValidIssuer = issUrl,
                        IssuerSigningKeys = jwk ,
                        ValidateLifetime = true,
                        ValidAudience = apiName
                    };
                    var handler = new JwtSecurityTokenHandler();
                    //2、使用公鑰校驗是否合法,如果驗證失敗會拋出異常
                    var id = handler.ValidateToken(access_token, parameters, out var _);
                    //請求的內容保存
                    actionContext.RequestContext.Principal = id;
                }
            }
            catch(Exception ex)
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未授權\"}");
            }
        }
    }

    public class Ids4Keys
    {
        public JsonWebKey[] keys { get; set; }
    }
}

代碼非常簡潔,就實現了基於Ids4的訪問控制,現在我們開始使用PostMan來測試介面地址。

我們直接請求介面地址,返回401未授權。

然後我使用Ids4生成的access_token再次測試,可以得到我們預期結果。

為了驗證是不是任何地方簽發的token都可以通過驗證,我使用其他項目生成的access_token來測試,發現提示的401未授權,可以達到我們預期結果。

現在就可以開心的使用我們熟悉的webapi開發我們的介面了,需要驗證的地方增加類似[Ids4Auth("http://localhost:6611", "mpc_gateway")]代碼即可。

使用其他語言實現的原理基本一致,就是公鑰來驗簽,只要通過驗證證明是允許訪問的請求,由於公鑰一直不變(除非認證伺服器更新了證書),所以我們請求到後可以緩存到本地,這樣驗簽時可以省去每次都獲取公鑰這步操作。

四、總結

本篇我們介紹了JWT的基本原理和Ids4JWT實現方式,然後使用.NET webapi實現了使用Ids4保護介面,其他語言實現方式一樣,這樣我們就可以把網關部署後,後端服務使用任何語言開發,然後接入到網關即可。

有了這些知識點,感覺是不是對Ids4的理解更深入了呢?JWT確實方便,但是有些特殊場景是我們希望Token在有效期內通過人工配置的方式立即失效,如果按照現有Ids4驗證方式是沒有辦法做到,那該如何實現呢?我將會在下一篇來介紹如何實現強制token失效,敬請期待吧。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們的目標網站是這個http://awehome.com.cn,登錄頁面是這個http://awehome.com.cn/tenant/login 搜索我們使用request的session來保存會話並且進入登錄頁面,他是這樣的 我們先來獲取驗證碼,直接通過html.text來獲取是找不到他裡面的圖 ...
  • 題意 "題目鏈接" 有$n$張牌,每張牌有四個屬性$(a, b, c, d)$,主人公有兩個屬性$(x, y)$(初始時為(0, 0)) 一張牌能夠被使用當且僅當$a define Pair pair define MP(x, y) make_pair(x, y) define fi first d ...
  • 簡介 資料庫查詢構建器提供了一個方便的流介面用於創建和執行資料庫查詢。查詢構建器可以用於執行應用中絕大部分資料庫操作,並且能夠在 Laravel 支持的所有資料庫系統上工作。 註:流介面是一種設計模式,更多關於流介面模式的設計和使用方式,可查看這篇教程:PHP 設計模式系列 —— 流介面模式。 La ...
  • 使用DB查詢,必須use Illuminate\Support\Facades\DB; 多數組條件查詢單條數據 first() 解釋下哈,這裡是前臺模板提交ajax提交來的post方式數據,這裡我沒有用到Model層,也就是Model層邏輯在Controller里寫了,這也是可以的哈 查詢多條數據用 ...
  • 1.個人思路 1.情景假設 一天,班主任佈置了一個任務:給所有的小朋友都發了一個牌子,每個牌子上都有一個數字,誰找到兩個牌子合起來的數字等於老師的牌子,那麼可以贏的一個蘋果。 這個時候小朋友,如果你是小朋友。肯定先看自己牌子的數字,然後在看看老師牌子的數字,找數學老師算出需要的數字,然後去看看誰的牌 ...
  • 上兩節介紹完Hybrid模式在MVC下的使用,包括驗證從數據獲取的User和Claim對MVC的身份授權。本節將介紹Implicit模式在JavaScript應用程式中的使用,使用Node.js+Express構建JavaScript客戶端,實現前後端分離。本節授權服務和資源伺服器基於第四和第五節。 ...
  • 在上一篇博文《 "[UWP]在UWP平臺中使用Lottie動畫" 》中我簡單介紹了一下LottieUWP項目以及如何使用它呈現Lottie動畫,這篇文章里我們來講點進階的東西——緩存Lottie動畫幀。 為什麼會有這樣的需求呢? 有兩方面原因: 直接在XAML中使用Lottie動畫時,是邊播放邊渲染 ...
  • 最近QQ影音久違的更新了,因為記得QQ影音之前體驗還算不錯(FFmepg的事另說),我也第一時間去官網下載體驗了一下,結果發現一些有趣的事情。 是的,你沒看錯,QQ影音主界面上這個動畫效果是使用Lottie動畫實現的! 這讓我大為驚奇,我對Lottie瞭解還算是比較多的,但是Lottie常見應用於移 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...