背景需求:
系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行呼叫,且後期還要提供給其他兄弟部門系統共同呼叫。
原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書等各種條件,此做法成本較大。
so:
為了解決對接的XXX官方API問題,我們搭建了一套中繼系統,顧名思義:就是一套用於請求中轉的中繼系統。在系統搭建的時,Leader提出要做多套鑑權方案,必須做到 動靜結合 身份鑑權。
動靜結合:就是動態Token 和 靜態固定Token。
動態Token:用於兄弟部門系統或對外訪問到此中繼系統申請的Token,供後期呼叫對應API。
固定Token:用於當前部門中的諸多子系統,提供一個超級Token,此Token長期有效,且不會隨意更換。
入坑:
因為剛來第一週我就接手了這個專案。專案處於申請賬號階段,即將進入開發。對接的是全英文文件(申請/對接流程/開發API....),文件複雜。當時我的感覺:OMG,這不得跑路?整個專案可謂難度之大。然後因為對內部業務也不熟悉,上手就看了微服務等相關係統程式碼,注:每套系統之間文件少的可憐,可以說系統無文件狀態。
專案移交的時候,Leader之說讓我熟悉並逐漸進入開發,讓我請教同事。好嘛,請教了同事。同事也是接了前任離職的文件而已,大家都不是很熟悉。於是同事讓我啟新的專案也是直接對接微服務形式開發,一頓操作猛如虎。
專案開發第二週,已經打出框架模型並對接了部分API。此時,Leader開會問進度,結果來一句:此專案使用獨立API方式執行,部署到Docker,不接入公司的微服務架構。好嘛,幾天功夫白費了,真是取其糟粕去其精華~,恢復成WebAPI。
技術實現:
因為之前對身份認證鑑權這一塊沒有做太多的深入瞭解,Leader工期也在屁股追,就一句話:怎麼快怎麼來,先上後迭代。好嘛,為了專案方便,同時為了符合動靜結合的身份認證鑑權 。於是,我用了 JWT+自定義身份認證 實現了需求。
方案一:多身份認證+中介軟體模式實現
新增服務:Services.AddAuthentication 預設使用JWT
//多重身份認證
//預設使用JWT,如果Controller使用 AuthenticationSchemes 則採用指定的身份認證
Services.AddAuthentication(options =>
{
options.AddScheme<CustomAuthenticationHandler>(CustomAuthenticationHandler.AuthenticationSchemeName, CustomAuthenticationHandler.AuthenticationSchemeName);
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;//設定後設資料地址或許可權是否需要HTTPs
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
};
options.Events = new CustomJwtBearerEvents();
});
自定義身份認證 CustomAuthenticationHandler.cs程式碼
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string AuthenticationSchemeName = "CustomAuthenticationHandler";
private readonly IConfiguration _configuration;
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IConfiguration configuration)
: base(options, logger, encoder, clock)
{
_configuration = configuration;
}
/// <summary>
/// 固定Token認證
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string isAnonymous = Request.Headers["IsAnonymous"].ToString();
if (!string.IsNullOrEmpty(isAnonymous))
{
bool isAuthenticated = Convert.ToBoolean(isAnonymous);
if (isAuthenticated)
return AuthenticateResult.NoResult();
}
string authorization = Request.Headers["Authorization"].ToString();
// "Bearer " --> Bearer後面跟一個空格
string token = authorization.StartsWith("Bearer ") ? authorization.Remove(0, "Bearer ".Length) : authorization;
if (string.IsNullOrEmpty(token))
return AuthenticateResult.Fail("請求頭Authorization不允許為空。");
//透過金鑰,進行加密、解密對比認證
if (!VerifyAuthorization(token))
return AuthenticateResult.Fail("傳入的Authorization身份驗證失敗。");
return AuthenticateResult.Success(GetTicket());
}
private AuthenticationTicket GetTicket()
{
// 驗證成功,建立身份驗證票據
var claims = new[]
{
new Claim(ClaimTypes.Role, "Admin"),
new Claim(ClaimTypes.Role, "Public"),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
return ticket;
}
private bool VerifyAuthorization(string token)
{
//token: [0]隨機生成64位字串,[1]載荷資料,[2]採用Hash對[0]+[1]的簽名
var tokenArr = token.Split('.');
if (tokenArr.Length != 3)
{
return false;
}
try
{
//1、先比對簽名串是否一致
string signature = tokenArr[1].Hmacsha256HashEncrypt().ToLower();
if (!signature.Equals(tokenArr[2].ToLower()))
{
return false;
}
//解密
var aecStr = tokenArr[1].Base64ToString();
var clientId = aecStr.DecryptAES();
//2、再驗證載荷資料的有效性
var clientList = _configuration.GetSection("FixedClient").Get<List<FixedClientSet>>();
var clientData = clientList.SingleOrDefault(it => it.ClientID.Equals(clientId));
if (clientData == null)
{
return false;
}
}
catch (Exception)
{
throw;
}
return true;
}
}
使用中介軟體:UseMiddleware
app.UseAuthentication();
//中介軟體模式:自定義認證中介軟體:雙重認證選其一
//如果使用 策略,需要註釋掉 中介軟體
app.UseMiddleware<FallbackAuthenticationMiddleware>(); //使用中介軟體實現
app.UseAuthorization();
中介軟體FallbackAuthenticationMiddleware.cs程式碼實現
public class FallbackAuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly IAuthenticationSchemeProvider _schemeProvider;
public FallbackAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemeProvider)
{
_next = next;
_schemeProvider = schemeProvider;
}
/// <summary>
/// 身份認證方案
/// 預設JWT。JWT失敗,執行自定義認證
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context)
{
var endpoints = context.GetEndpoint();
if (endpoints == null || !endpoints.Metadata.OfType<IAuthorizeData>().Any() || endpoints.Metadata.OfType<IAllowAnonymous>().Any())
{
await _next(context);
return;
}
//預設JWT。JWT失敗,執行自定義認證
var result = await Authenticate_JwtAsync(context);
if (!result.Succeeded)
result = await Authenticate_CustomTokenAsync(context);
// 設定認證票據到HttpContext中
if (result.Succeeded)
context.User = result.Principal;
await _next(context);
}
/// <summary>
/// JWT的認證
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task<dynamic> Authenticate_JwtAsync(HttpContext context)
{
var verify = context.User?.Identity?.IsAuthenticated ?? false;
string authenticationType = context.User.Identity.AuthenticationType;
if (verify && authenticationType != null)
{
return new { Succeeded = verify, Principal = context.User, Message = "" };
}
await Task.CompletedTask;
// 找不到JWT身份驗證方案,或者無法獲取處理程式。
return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "JWT authentication scheme not found or handler could not be obtained." };
}
/// <summary>
/// 自定義認證
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task<dynamic> Authenticate_CustomTokenAsync(HttpContext context)
{
// 自定義認證方案的名稱
var customScheme = "CustomAuthenticationHandler";
var fixedTokenHandler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, customScheme);
if (fixedTokenHandler != null)
{
var Res = await fixedTokenHandler.AuthenticateAsync();
return new { Res.Succeeded, Res.Principal, Res.Failure?.Message };
}
//找不到CustomAuthenticationHandler身份驗證方案,或者無法獲取處理程式。
return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "CustomAuthenticationHandler authentication scheme not found or handler could not be obtained." };
}
}
方案二:透過[Authorize]標籤的AuthenticationSchemes
因為中介軟體還要多維護一段中介軟體的程式碼,顯得略微複雜,於是透過[Authorize(AuthenticationSchemes = "")]方式。
//使用特定身份認證
//[Authorize(AuthenticationSchemes = CustomAuthenticationHandler.AuthenticationSchemeName)]
//任一身份認證
[Authorize(AuthenticationSchemes = $"{CustomAuthenticationHandler.AuthenticationSchemeName},{JwtBearerDefaults.AuthenticationScheme}")]
public class DataProcessingController : ControllerBase
{
}
方案二:透過[Authorize]標籤的policy
如果還有其他身份認證,那不斷增加AuthenticationSchemes拼接在Controller的頭頂,顯得不太好看,且要是多個Controller使用,也會導致維護麻煩,於是改用策略方式。
在Program.cs新增服務AddAuthorization。使用策略的好處是增加易維護性。
//授權策略
//Controller使用 policy 則採用指定的策略配置進行身份認證
builder.Services.AddAuthorization(option =>
{
option.AddPolicy(CustomPolicy.Policy_A, policy => policy
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName, JwtBearerDefaults.AuthenticationScheme)
);
option.AddPolicy(CustomPolicy.Policy_B, policy => policy
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName)
);
option.AddPolicy(CustomPolicy.Policy_C, policy => policy
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
);
});
//使用特定策略身份認證
[Authorize(policy:CustomPolicy.Policy_B)]
public class DataProcessingController : ControllerBase
{
}
/// <summary>
/// 策略類
/// </summary>
public static class CustomPolicy
{
public const string Policy_A= "Policy_A";
public const string Policy_B = "Policy_B";
public const string Policy_C = "Policy_C";
}
最後附上截圖:
新增服務:
使用中介軟體:
控制器:
這樣,整套中繼系統就能完美的滿足Leader的需求,且達到預期效果。
原始碼Demo:https://gitee.com/LaoPaoE/project-demo.git
最後附上:
AuthorizeAttribute 同時使用 Policy 和 AuthenticationSchemes 和 Roles 時是怎麼鑑權的流程:
- AuthenticationSchemes鑑權:
- AuthenticationSchemes 屬性指定了用於驗證使用者身份的認證方案(如Cookies、Bearer Tokens等)。
- ASP.NET Core會根據這些認證方案對使用者進行身份驗證。如果使用者未透過身份驗證(即未登入或未提供有效的認證資訊),則請求會被拒絕,並可能重定向到登入頁面。
- Roles鑑權(如果指定了Roles):
- 如果AuthorizeAttribute中還指定了 Roles 屬性,那麼除了透過身份驗證外,使用者還必須屬於這些角色之一。
- ASP.NET Core會檢查使用者的角色資訊,以確定使用者是否屬於 Roles 屬性中指定的一個或多個角色。
- Policy鑑權(如果指定了Policy):
- Policy 屬性指定了一個或多個授權策略,這些策略定義了使用者必須滿足的額外條件才能訪問資源。
- ASP.NET Core會呼叫相應的 IAuthorizationHandler 來評估使用者是否滿足該策略中的所有要求。這些要求可以基於角色、宣告(Claims)、資源等定義。
- 如果使用者不滿足策略中的任何要求,則授權失敗,並返回一個HTTP 403 Forbidden響應。
鑑權順序和組合
- 通常,AuthenticationSchemes的驗證會首先進行,因為這是訪問任何受保護資源的前提。
- 如果AuthenticationSchemes驗證透過,接下來會根據是否指定了Roles和Policy來進一步進行鑑權。
- Roles和Policy的鑑權順序可能因ASP.NET Core的具體版本和配置而異,但一般來說,它們會作為獨立的條件進行評估。
- 使用者必須同時滿足AuthenticationSchemes、Roles(如果指定)和Policy(如果指定)中的所有條件,才能成功訪問受保護的資源。
注意事項
- 在某些情況下,即使AuthenticationSchemes和Roles驗證都透過,但如果Policy中的要求未得到滿足,使用者仍然無法訪問資源。
- 可以透過自定義 IAuthorizationRequirement 和 IAuthorizationHandler 來實現複雜的授權邏輯,以滿足特定的業務需求。
- 確保在應用程式的身份驗證和授權配置中正確設定了AuthenticationSchemes、Roles和Policy,以便它們能夠協同工作,提供有效的訪問控制。