使用 Istio 進行 JWT 身份驗證(充當 API 網關)


博客已遷移至:https://ryan4yin.space/posts/use-istio-for-jwt-auth/

本文基於 Istio1.5 編寫測試

Istio 支持使用 JWT 對終端用戶進行身份驗證(Istio End User Authentication),支持多種 JWT 簽名算法。

目前主流的 JWT 算法是 RS256/ES256。(請忽略 HS256,該算法不適合分布式 JWT 驗證)

這里以 RSA256 算法為例進行介紹,ES256 的配置方式也是一樣的。

1. 介紹 JWK 與 JWKS

Istio 要求提供 JWKS 格式的信息,用於 JWT 簽名驗證。因此這里得先介紹一下 JWK 和 JWKS.

JWKS ,也就是 JWK Set,json 結構如下:

{
"keys": [
  <jwk-1>,
  <jwk-2>,
  ...
]}

JWKS 描述一組 JWK 密鑰。它能同時描述多個可用的公鑰,應用場景之一是密鑰的 Rotate.

而 JWK,全稱是 Json Web Key,它描述了一個加密密鑰(公鑰或私鑰)的各項屬性,包括密鑰的值。

Istio 使用 JWK 描述驗證 JWT 簽名所需要的信息。在使用 RSA 簽名算法時,JWK 描述的應該是用於驗證的 RSA 公鑰。

一個 RSA 公鑰的 JWK 描述如下:

{
    "alg": "RS256",  # 算法「可選參數」
    "kty": "RSA",    # 密鑰類型
    "use": "sig",    # 被用於簽名「可選參數」
    "kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg",  # key 的唯一 id
    "n": "yeNlzlub94YgerT030codqEztjfU_S6X4DbDA_iVKkjAWtYfPHDzz_sPCT1Axz6isZdf3lHpq_gYX4Sz-cbe4rjmigxUxr-FgKHQy3HeCdK6hNq9ASQvMK9LBOpXDNn7mei6RZWom4wo3CMvvsY1w8tjtfLb-yQwJPltHxShZq5-ihC9irpLI9xEBTgG12q5lGIFPhTl_7inA1PFK97LuSLnTJzW0bj096v_TMDg7pOWm_zHtF53qbVsI0e3v5nmdKXdFf9BjIARRfVrbxVxiZHjU6zL6jY5QJdh1QCmENoejj_ytspMmGW7yMRxzUqgxcAqOBpVm0b-_mW3HoBdjQ",
    "e": "AQAB"
}

RSA 是基於大數分解的加密/簽名算法,上述參數中,e 是公鑰的模數(modulus),n 是公鑰的指數(exponent),兩個參數都是 base64 字符串。

JWK 中 RSA 公鑰的具體定義參見 RSA Keys - JSON Web Algorithms (JWA)

2. JWK 的生成

要生成 JWK 公鑰,需要先生成私鑰,生成方法參見 JWT 簽名算法 HS256、RS256 及 ES256 及密鑰生成

公鑰不需要用上述方法生成,因為我們需要的是 JWK 格式的公鑰。后面會通過 python 生成出 JWK 公鑰。

上面的命令會將生成出的 RSA 私鑰寫入 key.pem 中,查看一下私鑰內容。

ryan@RYAN-MI-DESKTOP:~/istio$ cat key.pem
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAt1cKkQqPh8iOv5BhKh7Rx6A2+1ldpO/jczML/0GBKu4X+lHr
Y8YbJrt29jyAXlWM8vHC7tXsqgUG+WziRD0D8nhnh10XC14SeH+3mVuBqph+TqhX
TWsh9gtAIbeUHJjEI4I79QK4/wquPHHIGZBQDQQnuMh6vAS3VaUYJdEIoKvUBnAy
Y35kJZgyJSbrxLsEExL2zujUD/OY+/In2bq/3rFtDGNlgHyC7Gu2zXSXvfOA4O5m
9BBXOc7eEqj7PoOKNaTxLN3YcuRtgR6NIXL4KLb6oyvIzoeiprt4+9q7sc3Dnkc5
EV9kwWlEW2DHzhP6VYca0WXIIXc53U1AM3ewxwIDAQABAoIBABIKhaaqJF+XM7zU
B0uuxrPfJynqrFVbqcUfQ9H1bzF7Rm7CeuhRiUBxeA5Y+8TMpFcPxT/dWzGL1xja
RxWx715/zKg8V9Uth6HF55o2r/bKlLtGw3iBz1C34LKwrul1eu+HlEDS6MNoGKco
BynE0qvFOedsCu/Pgv7xhQPLow60Ty1uM0AhbcPgi6yJ5ksRB1XjtEnW0t+c8yQS
nU3mU8k230SdMhf4Ifud/5TPLjmXdFpyPi9uYiVdJ5oWsmMWEvekXoBnHWDDF/eT
VkVMiTBorT4qn+Ax1VjHL2VOMO5ZbXEcpbIc3Uer7eZAaDQ0NPZK37IkIn9TiZ21
cqzgbCkCgYEA5enHZbD5JgfwSNWCaiNrcBhYjpCtvfbT82yGW+J4/Qe/H+bY/hmJ
RRTKf0kVPdRwZzq7GphVMWIuezbOk0aFGhk/SzIveW8QpLY0FV/5xFnGNjV9AuNc
xrmgVshUsyQvr1TFkbdkC6yuvNgQfXfnbEoaPsXYEMCii2zqdF5lWGUCgYEAzCR2
6g8vEQx0hdRS5d0zD2/9IRYNzfP5oK0+F3KHH2OuwlmQVIo7IhCiUgqserXNBDef
hj+GNcU8O/yXLomAXG7VG/cLWRrpY8d9bcRMrwb0/SkNr0yNrkqHiWQ/PvR+2MLk
viWFZTTp8YizPA+8pSC/oFd1jkZF0UhKVAREM7sCgYB5+mfxyczFopyW58ADM7uC
g0goixXCnTuiAEfgY+0wwXVjJYSme0HaxscQdOOyJA1ml0BBQeShCKgEcvVyKY3g
ZNixunR5hrVbzdcgKAVJaR/CDuq+J4ZHYKByqmJVkLND4EPZpWSM1Rb31eIZzw2W
5FG8UBbr/GfAdQ6GorY+CQKBgQCzWQHkBmz6VG/2t6AQ9LIMSP4hWEfOfh78q9dW
MDdIO4JomtkzfLIQ7n49B8WalShGITwUbLDTgrG1neeQahsMmg6+X99nbD5JfBTV
H9WjG8CWvb+ZF++NhUroSNtLyu+6LhdaeopkbQVvPwMArG62wDu6ebv8v/5MrG8o
uwrUSwKBgQCxV43ZqTRnEuDlF7jMN+2JZWhpbrucTG5INoMPOC0ZVatePszZjYm8
LrmqQZHer2nqtFpyslwgKMWgmVLJTH7sVf0hS9po0+iSYY/r8e/c85UdUreb0xyT
x8whrOnMMODCAqu4W/Rx1Lgf2vXIx0pZmlt8Df9i2AVg/ePR6jO3Nw==
-----END RSA PRIVATE KEY-----

接下來通過 Python 編程生成 RSA Public Key 和 JWK(jwk 其實就是公鑰的另一個表述形式而已):

# 需要先安裝依賴: pip install jwcrypto
from jwcrypto.jwk import JWK
from pathlib import Path

private_key = Path("key.pem").read_bytes()
jwk = JWK.from_pem(private_key)

# 導出公鑰 RSA Public Key
public_key = jwk.public().export_to_pem()
print(public_key)

print("="*30)

# 導出 JWK
jwk_bytes = jwk.public().export()
print(jwk_bytes)

Istio 需要 JWK 進行 JWT 驗證,而我們手動驗證 JWT 時一般需要用到 Public Key. 方便起見,上述代碼把這兩個都打印了出來。內容如下:

# Public Key 內容,不包含這行注釋
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1cKkQqPh8iOv5BhKh7R
x6A2+1ldpO/jczML/0GBKu4X+lHrY8YbJrt29jyAXlWM8vHC7tXsqgUG+WziRD0D
8nhnh10XC14SeH+3mVuBqph+TqhXTWsh9gtAIbeUHJjEI4I79QK4/wquPHHIGZBQ
DQQnuMh6vAS3VaUYJdEIoKvUBnAyY35kJZgyJSbrxLsEExL2zujUD/OY+/In2bq/
3rFtDGNlgHyC7Gu2zXSXvfOA4O5m9BBXOc7eEqj7PoOKNaTxLN3YcuRtgR6NIXL4
KLb6oyvIzoeiprt4+9q7sc3Dnkc5EV9kwWlEW2DHzhP6VYca0WXIIXc53U1AM3ew
xwIDAQAB
-----END PUBLIC KEY-----
# jwk 內容
{
 'e': 'AQAB',
 'kid': 'oyYwZSLCLVVPHdVp0jXIcLNpGn6dMCumlY-6wSenmFo',
 'kty': 'RSA',
 'n': 't1cKkQqPh8iOv5BhKh7Rx6A2-1ldpO_jczML_0GBKu4X-lHrY8YbJrt29jyAXlWM8vHC7tXsqgUG-WziRD0D8nhnh10XC14SeH-3mVuBqph-TqhXTWsh9gtAIbeUHJjEI4I79QK4_wquPHHIGZBQDQQnuMh6vAS3VaUYJdEIoKvUBnAyY35kJZgyJSbrxLsEExL2zujUD_OY-_In2bq_3rFtDGNlgHyC7Gu2zXSXvfOA4O5m9BBXOc7eEqj7PoOKNaTxLN3YcuRtgR6NIXL4KLb6oyvIzoeiprt4-9q7sc3Dnkc5EV9kwWlEW2DHzhP6VYca0WXIIXc53U1AM3ewxw'
}

4. 測試密鑰可用性

接下來在 jwt.io 中填入測試用的公鑰私鑰,還有 Header/Payload。一是測試公私鑰的可用性,二是生成出 JWT 供后續測試 Istio JWT 驗證功能的可用性。

可以看到左下角顯示「Signature Verified」,成功地生成出了 JWT。后續可以使用這個 JWT 訪問 Istio 網關,測試 Istio JWT 驗證功能。

5. 啟用 Istio 的身份驗證

編寫 istio 配置:

apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
  name: "jwt-example"
  namespace: istio-system  # istio-system 名字空間中的配置,默認情況下會應用到所有名字空間
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  jwtRules:
  # issuer 即簽發者,需要和 JWT payload 中的 iss 屬性完全一致。
  - issuer: "testing@secure.istio.io"
    jwks: |
    {
        "keys": [
            {
                "e": "AQAB",
                "kid": "oyYwZSLCLVVPHdVp0jXIcLNpGn6dMCumlY-6wSenmFo",  # kid 需要與 jwt header 中的 kid 完全一致。
                "kty": "RSA",
                "n": "t1cKkQqPh8iOv5BhKh7Rx6A2-1ldpO_jczML_0GBKu4X-lHrY8YbJrt29jyAXlWM8vHC7tXsqgUG-WziRD0D8nhnh10XC14SeH-3mVuBqph-TqhXTWsh9gtAIbeUHJjEI4I79QK4_wquPHHIGZBQDQQnuMh6vAS3VaUYJdEIoKvUBnAyY35kJZgyJSbrxLsEExL2zujUD_OY-_In2bq_3rFtDGNlgHyC7Gu2zXSXvfOA4O5m9BBXOc7eEqj7PoOKNaTxLN3YcuRtgR6NIXL4KLb6oyvIzoeiprt4-9q7sc3Dnkc5EV9kwWlEW2DHzhP6VYca0WXIIXc53U1AM3ewxw"
            }
        ]
    }
      # jwks 或 jwksUri 二選其一
      # jwksUri: "http://nginx.test.local/istio/jwks.json"

現在 kubectl apply 一下,JWT 驗證就添加到全局了。

可以看到 jwtRules 是一個列表,因此可以為每個 issuers 配置不同的 jwtRule.

對同一個 issuers(jwt 簽發者),可以通過 jwks 設置多個公鑰,以實現JWT簽名密鑰的輪轉。
JWT 的驗證規則是:

  1. JWT 的 payload 中有 issuer 屬性,首先通過 issuer 匹配到對應的 istio 中配置的 jwks。
  2. JWT 的 header 中有 kid 屬性,第二步在 jwks 的公鑰列表中,中找到 kid 相同的公鑰。
  3. 使用找到的公鑰進行 JWT 簽名驗證。

6. 啟用 Payload 轉發/Authorization 轉發

默認情況下,Istio 在完成了身份驗證之后,會去掉 Authorization 請求頭再進行轉發。
這將導致我們的后端服務獲取不到對應的 Payload,無法判斷 End User 的身份。
因此我們需要啟用 Istio 的 Authorization 請求頭的轉發功能,在前述的 RequestAuthentication yaml 配置中添加一個參數就行:

apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
  name: "jwt-example"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  jwtRules:
  - issuer: "testing@secure.istio.io"
    jwks: |
    {
        "keys": [
            {
                "e": "AQAB",
                "kid": "oyYwZSLCLVVPHdVp0jXIcLNpGn6dMCumlY-6wSenmFo",
                "kty": "RSA",
                "n": "t1cKkQqPh8iOv5BhKh7Rx6A2-1ldpO_jczML_0GBKu4X-lHrY8YbJrt29jyAXlWM8vHC7tXsqgUG-WziRD0D8nhnh10XC14SeH-3mVuBqph-TqhXTWsh9gtAIbeUHJjEI4I79QK4_wquPHHIGZBQDQQnuMh6vAS3VaUYJdEIoKvUBnAyY35kJZgyJSbrxLsEExL2zujUD_OY-_In2bq_3rFtDGNlgHyC7Gu2zXSXvfOA4O5m9BBXOc7eEqj7PoOKNaTxLN3YcuRtgR6NIXL4KLb6oyvIzoeiprt4-9q7sc3Dnkc5EV9kwWlEW2DHzhP6VYca0WXIIXc53U1AM3ewxw"
            }
        ]
    }
# ===================== 添加如下參數===========================
    forwardOriginalToken: true  # 轉發 Authorization 請求頭

加了轉發后,流程圖如下(需要 mermaid 渲染):

sequenceDiagram # autonumber participant User as 用戶 participant Auth as 授權服務 participant IG as IngressGateway participant SVC as 某服務 User->>+Auth: Login Auth->>Auth: 用私鑰生成 JWT 簽名 Auth-->>-User: 返回 JWT User->>+IG: 請求信息(帶 JWT) IG->>IG: 用公鑰驗證 JWT 簽名 IG->>-SVC: 請求信息(轉發 JWT) SVC-->>IG: 返回信息 IG-->>User: 返回信息

其他問題

1. AuthorizationPolicy

Istio 的 JWT 驗證規則,默認情況下會直接忽略不帶 Authorization 請求頭的流量,因此這類流量能直接進入網格內部。如果需要禁止不帶 Authorization 頭的流量,需要額外配置 AuthorizationPolicy 策略。

RequestsAuthentication 驗證失敗的請求,Istio 會返回 401 狀態碼。
AuthorizationPolicy 驗證失敗的請求,Istio 會返回 403 狀態碼。

這會導致在使用 AuthorizationPolicy 禁止了不帶 Authorization 頭的流量后,這類請求會直接被返回 403。。。在使用 RESTful API 時,這種情況可能會造成一定的問題。

2. Response Headers

RequestsAuthentication 不支持自定義響應頭信息,這導致對於前后端分離的 Web API 而言,
一旦 JWT 失效,Istio 會直接將 401 返回給前端 Web 頁面。
因為響應頭中不包含 Access-Crontrol-Allow-Origin,響應將被瀏覽器攔截!

這可能需要通過 EnvoyFilter 自定義響應頭,添加跨域信息。

3. API 白名單

部分 API 可能不需要進行 JWT 認證,只要它們不帶有 JWT 請求頭,通常都能直接被轉發到集群內部。
特殊情況,Istio 也有提供 API 過濾器的功能,詳見官方文檔,后續有空補充。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM