一.前言
本文為系列補坑之作,拖了許久決定先把坑填完。
下文示範所用代碼采用的 IdentityServer4 版本為 2.3.0,由于時間推移可能以後的版本會有一些改動,請參考檢視,文末附上Demo代碼。
本文所訴Token如無特殊說明皆為 JWT。
衆所周知 JWT Token 由三部分組成,第一部分 Header,包含 keyid、簽名算法、Token類型;第二部分 Payload 包含 Token 的資訊主體,如授權時間、過期時間、頒發者、身份唯一辨別等等;第三部分是Token的簽名。我們對一個 Token 進行解碼,觀察其中 Payload 部分,你将會發現一個 "iss" 字段,那麼它代表什麼呢,它又有什麼作用呢,請看後文分解。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL4gTM3YzNzAzNtATN2kDNxIzNxAzMxATOxAjMtQDMxgjN28CXxATOxAjMvwFNwEDO2YzLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
二. Issuer 的前世今生
iss 是 OpenId Connect(後文簡稱OIDC)協定中定義的一個字段,其全稱為 “Issuer Identifier”,中文意思就是:頒發者身份辨別,表示 Token 頒發者的唯一辨別,一般是一個 http(s) url,如
https://www.baidu.com
。
在 Token 的驗證過程中,會将它作為驗證的一個階段,如無法比對将會造成驗證失敗,最後傳回 HTTP 401。
三. Issuer 的驗證流程分析
JWT的驗證是去中心化的驗證,實際這個驗證過程是發生在API資源的,除了必要的從 IdentityServer4 擷取中繼資料(擷取後會緩存,不用重複擷取)比如擷取公鑰用于驗證簽名,是不會再去互動的(詳細介紹:https://www.cnblogs.com/stulzq/p/9226059.html)。那麼我們就從 API 資源作為入口開始分析。
我們在 API 資源的配置認證的代碼如下:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
// IdentityServer4 位址
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvc();
}
}
}
一個攜帶 Token 的請求從認證中間件到最終驗證 Issuer 的邏輯如下圖(懶得畫流程圖了,直接做個gif)。
(新視窗打開檢視大圖)
最終驗證 Issuer 的代碼:
public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
//最終進入 驗證Issuer邏輯
if (validationParameters == null)
throw LogHelper.LogArgumentNullException(nameof(validationParameters));
if (!validationParameters.ValidateIssuer)
{
LogHelper.LogInformation(LogMessages.IDX10235);
return issuer;
}
if (validationParameters.IssuerValidator != null)
return validationParameters.IssuerValidator(issuer, securityToken, validationParameters);
if (string.IsNullOrWhiteSpace(issuer))
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10211)
{ InvalidIssuer = issuer });
// Throw if all possible places to validate against are null or empty
if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) && (validationParameters.ValidIssuers == null))
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10204)
{ InvalidIssuer = issuer });
if (string.Equals(validationParameters.ValidIssuer, issuer, StringComparison.Ordinal))
{
LogHelper.LogInformation(LogMessages.IDX10236, issuer);
return issuer;
}
if (null != validationParameters.ValidIssuers)
{
foreach (string str in validationParameters.ValidIssuers)
{
if (string.IsNullOrEmpty(str))
{
LogHelper.LogInformation(LogMessages.IDX10235);
continue;
}
if (string.Equals(str, issuer, StringComparison.Ordinal))
{
LogHelper.LogInformation(LogMessages.IDX10236, issuer);
return issuer;
}
}
}
throw LogHelper.LogExceptionMessage(
new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX10205, issuer, (validationParameters.ValidIssuer ?? "null"), Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)))
{ InvalidIssuer = issuer });
}
由源碼分析可以獲得幾個結論:
1.驗證 Token 必定會驗證 Issuer,如果 Issuer 驗證失敗,那麼表示則整個 Token 的驗證結果就是失敗。
- Issuer 預設從 IdentityServer4 的 Discovery Endpoint(/.well-known/openid-configuration)擷取Issuer
3.Issuer 可以自定義,并且可以設定一個清單,如果手動設定了會覆寫預設值
4.Issuer 驗證邏輯預設隻驗證是否相等,即 Token 攜帶的 Issuer 是否與 設定的 Issuer 值相等。
5.Issuer 驗證邏輯可以自定義
6.Issuer 的驗證可以關閉
以上設定如無特殊需求直接使用預設值即可,不需要額外設定。
關于以上結論的在代碼(API資源)中的實作:
四.如何設定 Token 的 Issuer
第三節講的是 Issuer 驗證時有效 Issuer 的設定,本節講的是 設定 Token 的 Issuer,Token攜帶的 Issuer 與API資源設定的有效 Issuer 進行驗證比對完成整個流程,這裡提一下,避免搞混。
設定 Token 的 Issuer 需要在 IdentityServer4 設定。在 Startup 裡中設定:
services.AddIdentityServer(option=>option.IssuerUri="https://www.baidu.com")
此值必須是一個 http(s) url。
驗證是否生效:
1.通路 Discovery Endpoint(/.well-known/openid-configuration)
2.對Token解碼,檢視 iss 字段
如果在 IdentityServer4 設定此值,預設情況下所有API資源都會擷取此值作為預設有效Issuer。
如果你自定義了 Issuer,在使用 Client 通路時會出現 Issuer 與 Authority 不比對的錯誤,是因為Client在預設情況下作了限制,關閉即可:
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest(){Address = "http://localhost:5000" ,Policy = new DiscoveryPolicy(){ValidateIssuerName = false}});
五.預設值問題
如果沒有手動設定 IdentityServer4 IssuerUri 值那麼它預設會取你通路 IdentityServer4 時的 Host,下面舉例說明。
首先修改 IdentityServer4 項目的監聽位址,使其能夠通過區域網路IP通路
然後分别通過 localhost 和 區域網路ip 通路 Discovery Endpoint,觀察 Issuer 的值:
localhost:
區域網路IP:
看出差異了吧,這一點需要注意,下一節将會講一下這個引發的問題。
六.Issuer 預設值問題可能出現的場景及解決
這種情況一般出現在 IdentityServer4 經過了一層或多層代理,比如 Nginx反代、網關等,外網位址經過代理傳遞到了 IdentityServer4,如果直接通過外網請求的 Token Endpoint(/connect/token) 生成的 Token,那麼這個 Token 攜帶的 iss 位址将會是外網位址(正常情況下,Host是會經過代理傳過來的,如果你不配置傳過來,那麼就沒有這個問題,那麼你的後端服務擷取的位址與預期肯定有差别,不推薦這種做法)。但是本地API資源(與IdentityServer4在同一台伺服器或者同一個區域網路)與IdentityServer4互動的位址(Authority)肯定會配成localhost 或者是區域網路位址(如果你這裡配置成外網位址,那麼你可以不繼續往下看了,内部互動還要走外部網絡嚴重不推薦甚至是禁止此種做法)。
上圖的架構即便是把 Gateway、IdentityServer、Basket服務(API資源)放在一台機器上也是一樣的道理,都會出現這種情況,其原因就是如果 IdentityServer 不設定 Issuer,就會取你通路IdentityServer時的Host作為Issuer,外網進來的Host位址和你内部互動的不一樣就造成了這個問題,解決辦法就是在 IdentityServer 手動指定一個 Issuer 即可解決(第四節),取消掉它的預設取Host的機制,不管你怎麼通路IdentityServer傳回的Issuer都是一個位址。
七.結束
Demo:
https://github.com/stulzq/IdentityServer4.Samples/tree/master/Practice/03_Issuer
參考資料:
OIDC(OpenId Connect)身份認證授權(核心部分) by blackheart.
最後如果你覺得有用請點選右下角的“推薦”支援一下,十分感謝,寫這篇部落格花了不少功夫。
目前學習.NET Core 最好的教程 .NET Core 官方教程 ASP.NET Core 官方教程
.NET Core 交流群:923036995 歡迎加群交流
如果您認為這篇文章還不錯或者有所收獲,您可以點選右下角的【推薦】支援,或請我喝杯咖啡【贊賞】,這将是我繼續寫作,分享的最大動力!
作者:曉晨Master(李志強)
聲明:原創部落格請在轉載時保留原文連結或者在文章開頭加上本人部落格位址,如發現錯誤,歡迎批評指正。凡是轉載于本人的文章,不能設定打賞功能,如有特殊需求請與本人聯系!