ASP.NET Core 基于角色的 JWT 令牌

原文:https://bit.ly/3vYljq3
作者:Rick Strahl
翻譯:精致碼農(nóng)-王亮
聲明:我翻譯技術(shù)文章不是逐句翻譯的,而是根據(jù)我自己的理解來(lái)表述的。其中可能會(huì)去除一些本人實(shí)在不知道如何組織但又不影響理解的句子。
ASP.NET Core 中的認(rèn)證和授權(quán)仍然是配置中最麻煩的組件。似乎幾乎在每一個(gè)應(yīng)用程序上,我都會(huì)遇到一些與 Auth 有關(guān)的問(wèn)題。四個(gè)版本帶來(lái)了三種不同的身份驗(yàn)證實(shí)現(xiàn),功能的更新也留下了一大波過(guò)時(shí)的信息。今天,我看著 Web API 基于角色 JWT 授權(quán)認(rèn)證的過(guò)時(shí)信息,陷入了一個(gè)土撥鼠日(譯注:形容不斷重復(fù)的日子)的循環(huán)中。
目前在 ASP.NET Core 中的 JWT 令牌(Token)配置實(shí)際上非常好用,只要你把正確的配置咒語(yǔ)串起來(lái)。Auth 配置的部分問(wèn)題是,大多數(shù)配置只需按固定的“儀式”進(jìn)行操作。例如,設(shè)置Issuer和Audience我們似乎完全不需要關(guān)心它們是什么,但它們是 JWT 令牌要求的一部分,確實(shí)需要配置。幸運(yùn)的是,這些設(shè)置中只有少數(shù)幾個(gè)是真正需要的,大部分都是模板。
在這篇文章中,我具體講一下:
ASP.NET Core Web API 的認(rèn)證
JWT 令牌的使用
基于角色授權(quán)
只使用底層功能--不使用 ASP.NET Core Identity
配置
認(rèn)證(Authentication)和授權(quán)(Authorization)在 ASP.NET Core 中作為中間件提供,你必須在ConfigureServices()中配置它們,并在Configure()中連接中間件。
配置 JWT 認(rèn)證和授權(quán)
第一步是在Startup文件中的ConfigureServices()中配置認(rèn)證(Authentication)。在這里添加 JWT 令牌配置,并將所需組件添加到 ASP.NET Core 的處理管道中:
// in ConfigureServices()
// config shown for reference values
config.JwtToken.Issuer = "https://mysite.com";
config.JwtToken.Audience = "https://mysite.com";
config.JwtToken.SigningKey = "12345@4321"; // some long id
// Configure Authentication
services.AddAuthentication( auth=>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer,
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
}
JWT 認(rèn)證有一堆的設(shè)置,其中大部分是足夠神秘的,所以我?guī)缀踔皇菍⑺鼈儚?fù)制和粘貼。我只想說(shuō),這些設(shè)置大多是關(guān)于設(shè)置協(xié)議和令牌包裝器(Wrapper)的。通常情況下,我將這些值存儲(chǔ)在我的應(yīng)用程序的配置中,這樣它就會(huì)通過(guò) .NET 配置 Provider 提取進(jìn)來(lái),而上面的config就是那個(gè)特定的配置實(shí)例。
在這個(gè)全局配置中沒(méi)有什么是針對(duì)角色的。所有基于角色的相關(guān)配置都發(fā)生在后面的認(rèn)證(Authenticate)端點(diǎn)中創(chuàng)建令牌的時(shí)候。
令牌和哈希如何工作
在進(jìn)入這里之前,我們先來(lái)回顧一下基于令牌的身份驗(yàn)證是如何工作的,以及這些設(shè)置值是如何融入這個(gè)方案的。
上面的設(shè)置值配置了令牌的常用值和用于簽署令牌的密鑰。它們提供身份識(shí)別標(biāo)記,以確保生成的令牌是唯一的。我認(rèn)為這些值是一個(gè)基本的令牌包裝,通常在你驗(yàn)證用戶(hù)后,當(dāng)你創(chuàng)建令牌并將令牌作為 Web 請(qǐng)求的一部分提供給用戶(hù)之時(shí),你將向令牌添加你的自定義、應(yīng)用特定的 Claim,。
IssuerSigningKey是這個(gè)配置中最重要的部分,它用于將最終的令牌與包裝器以及任何添加的聲明進(jìn)行哈希(Hash)。該哈希值用于驗(yàn)證令牌的真實(shí)性。請(qǐng)注意,雖然生成的令牌被編碼為 Base64,但它本身并不安全,即使在客戶(hù)端,內(nèi)容也可以被解碼。也就是說(shuō),你可以將任何 JWT 令牌粘貼到 JWT.io 這個(gè)網(wǎng)站中,對(duì)令牌的內(nèi)容進(jìn)行解碼。
哈希確保了令牌不能被改變。當(dāng)令牌與請(qǐng)求一起發(fā)送時(shí),它將由 ASP.NET Core 的 JWToken 中間件進(jìn)行驗(yàn)證,它首先根據(jù)令牌數(shù)據(jù)驗(yàn)證哈希值,然后根據(jù)包含的授權(quán)信息進(jìn)行認(rèn)證/授權(quán)。如果客戶(hù)端或其他實(shí)體以任何方式更改了令牌,則哈希值將無(wú)法驗(yàn)證通過(guò),會(huì)被直接拒絕。之后在中間件管道的授權(quán)部分進(jìn)行用戶(hù)名和角色等的匹配。
添加 Auth 中間件
接下來(lái)我們需要在Startup文件的Configure中使用app.UseAuthentication()和app.UseAuthorization()添加實(shí)際的中間件:
// in Startup.Configure()
app.UseHttpsRedirection();
app.UseRouting();
// *** These are the important ones - note order matters ***
app.UseAuthentication();
app.UseAuthorization();
app.UseStatusCodePages();
//app.UseDefaultFiles(); // so index.html is not required
//app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
請(qǐng)注意,順序?qū)τ谡J(rèn)證(Authentication)和授權(quán)(Authorization)很重要。這兩個(gè)需要在 Routing 之后但在任何 HTTP 輸出中間件之前添加,最重要的是在app.UseEndpoints()之前。
使用 Web API 端點(diǎn)認(rèn)證用戶(hù)
接下來(lái),我們需要在應(yīng)用程序中通過(guò)詢(xún)問(wèn)憑證來(lái)驗(yàn)證用戶(hù),然后生成一個(gè)令牌并將其返回給 API 客戶(hù)端。
這很可能發(fā)生在 Controller 的 Action 方法或中間件端點(diǎn)處理程序中。下面是使用 Controller 的 Action 方法示例:
[AllowAnonymous]
[HttpPost]
[Route("authenticate")]
public object Authenticate(AuthenticateRequestModel loginUser)
{
// My application logic to validate the user
// returns a user entity with Roles collection
var bus = new AccountBusiness();
var user = bus.AuthenticateUser(loginUser.Username, loginUser.Password);
if (user == null)
throw new ApiException("Invalid Login Credentials: " + bus.ErrorMessage, 401);
var claims = new List<Claim>();
claims.Add(new Claim("Username",loginUser.Username));
claims.Add(new Claim("DisplayName",loginUser.Name));
// Add roles as multiple claims
foreach(var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
// Optionally add other app specific claims as needed
claims.Add(new Claim("UserState", UserState.ToString()));
// create a new token with token helper and add our claim
// from `Westwind.AspNetCore` NuGet Package
var token = JwtHelper.GetJwtToken(
loginUser.Username,
Configuration.JwtToken.SigningKey,
Configuration.JwtToken.Issuer,
Configuration.JwtToken.Audience,
TimeSpan.FromMinutes(Configuration.JwtToken.TokenTimeoutMinutes),
claims.ToArray());
return new
{
token = JwtHelper.GetJwtTokenString(token),
expires = token.ValidTo
};
}
我正在使用一個(gè)JwtHelper類(lèi)來(lái)實(shí)際生成一個(gè)令牌,這樣我就不必在每個(gè)應(yīng)用中記住JwtHelper類(lèi)實(shí)現(xiàn)的這個(gè)重復(fù)的“儀式”。這段代碼創(chuàng)建了令牌,并從中提取了一個(gè)字符串,準(zhǔn)備作為承載令牌值返回。下面是這個(gè)類(lèi)的完整代碼:
public class JwtHelper
{
/// <summary>
/// Returns a Jwt Token from basic input parameters
/// </summary>
public static JwtSecurityToken GetJwtToken(
string username,
string uniqueKey,
string issuer,
string audience,
TimeSpan expiration,
Claim[] additionalClaims = null)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub,username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
if (additionalClaims is object)
{
var claimList = new List<Claim>(claims);
claimList.AddRange(additionalClaims);
claims = claimList.ToArray();
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(uniqueKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
return new JwtSecurityToken(
issuer: issuer,
audience: audience,
expires: DateTime.UtcNow.Add(expiration),
claims: claims,
signingCredentials: creds
);
}
/// <summary>
/// Returns a token string from base claims
/// </summary>
public static string GetJwtTokenString(
string username,
string uniqueKey,
string issuer,
string audience,
TimeSpan expiration,
Claim[] additionalClaims = null)
{
var token = GetJwtToken(username, uniqueKey, issuer, audience, expiration, additionalClaims);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// Converts an existing Jwt Token to a string
/// </summary>
public static string GetJwtTokenString(JwtSecurityToken token)
{
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// Returns an issuer key
/// </summary>
public static SymmetricSecurityKey GetSymetricSecurityKey(string issuerKey)
{
return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(issuerKey));
}
}
Controller 的Authenticate()代碼首先使用一個(gè)應(yīng)用程序特定的業(yè)務(wù)對(duì)象來(lái)驗(yàn)證用戶(hù),用戶(hù)登錄信息作為 API 調(diào)用的一部分傳入該方法(比如 HTML 網(wǎng)頁(yè)的登錄表單)。如果用戶(hù)是有效的,我就創(chuàng)建新的 Claim,這些 Claim 被打包到令牌中。
令牌包括用戶(hù)名和角色,這是 ASP.NET Core 授權(quán)工作所需的內(nèi)容。然后,如果有必要,我可以添加一些額外的應(yīng)用程序特定的 Claim,比如上面例子中的DisplayName和自定義UserState對(duì)象。這些聲明會(huì)隨令牌一起,以便在后續(xù)請(qǐng)求提取,而不必再訪問(wèn)后端數(shù)據(jù)庫(kù)檢索它們。
最后,使用JwtHelper的GetJwtToken()生成令牌,并使用GetJwtTokenString()將令牌轉(zhuǎn)換為字符串,這個(gè)字符串將被客戶(hù)端放在請(qǐng)求頭中攜帶到后臺(tái)服務(wù)端。
請(qǐng)注意,要確??梢阅涿L問(wèn)
Authentication方法。如果 Controller 標(biāo)注了[Authorize]特性,則需要在Authenticate()方法上標(biāo)注[AllowAnonymous]特性。
Claim 和角色
ASP.NET Core 使用 Claim 進(jìn)行認(rèn)證。Claim 是你可以存儲(chǔ)在令牌中的數(shù)據(jù)片段,這些數(shù)據(jù)與令牌一起攜帶,并可以從令牌中讀取。對(duì)于授權(quán)來(lái)說(shuō),角色可以作為 Claim。
在 .NET Core 3.1 和 5.x 中,為授權(quán)添加 ASP.NET Core 角色識(shí)別的正確語(yǔ)法是,為每個(gè)角色添加多個(gè) Claim:
// Add roles as multiple claims
foreach(var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
// these also work - and reduce token size
// claims.Add(new Claim("roles", role.Name));
// claims.Add(new Claim("role", role.Name));
}
訪問(wèn)生成 JWT 令牌的 API
到這,我已經(jīng)有了一個(gè)用于認(rèn)證的 API 端點(diǎn),我可以從這個(gè)端點(diǎn)上獲取一個(gè)令牌。下面是這個(gè)請(qǐng)求的樣子:

傳入用戶(hù)名和密碼,則會(huì)返回令牌和到期時(shí)間。你可以在 jwt.io 查看這個(gè)令牌和它生成的內(nèi)容:

請(qǐng)注意,該令牌很容易被外部工具解碼,與我的應(yīng)用程序完全無(wú)關(guān)。這意味著所包含的令牌數(shù)據(jù)是不安全的。然而,除非數(shù)據(jù)由原始的簽名密鑰簽名,否則無(wú)法更改該令牌中的值并提供給服務(wù)器應(yīng)用程序。這可以防止令牌被篡改。
一旦生成了令牌并發(fā)送給客戶(hù)端,客戶(hù)端就可以在后續(xù)的請(qǐng)求中使用它來(lái)添加相應(yīng)的授權(quán)請(qǐng)求頭:
Authorization: Bearer 123456******
確保 API 的安全
現(xiàn)在剩下的就是通過(guò)在 Controller 或端點(diǎn)方法上添加[Authorize]特性來(lái)選擇性或限制對(duì) API 的訪問(wèn)。
我可以使用以下特性之一,或者完全不使用特性(對(duì)于開(kāi)放訪問(wèn)):
普通的
[Authorize]讓任何經(jīng)過(guò)認(rèn)證的用戶(hù)進(jìn)入基于角色的
[Authorize(Roles = "Administrator,ReportUser")]訪問(wèn)允許匿名
[AllowAnonymous]訪問(wèn)
請(qǐng)注意,這些特性可以在 Controller 類(lèi)或 Action 方法上標(biāo)注,而且它們是自上而下分層工作的,所以一個(gè)類(lèi)屬性適用于所有的 Action 方法。這就是 [AllowAnonymous] 的用武之地,它可以覆蓋一兩個(gè)可能需要開(kāi)放訪問(wèn)的請(qǐng)求(如Authenticate()或Logout())。
要為任何登錄用戶(hù)設(shè)置授權(quán),只需使用[Authorize]即可:
[Authorize] // just require ANY authentication
[Route("/api/v1/lookups")]
public class IdLookupController : BaseApiController
在這種情況下,你可能需要對(duì)用戶(hù)進(jìn)行一些額外的驗(yàn)證,以確保你有正確的用戶(hù)進(jìn)行特定的操作。
要設(shè)置特定角色的限制,你可以使用Roles參數(shù):
[Authorize(Roles = "Administrator")]
[HttpPost]
[Route("customers")]
public async Task<SaveResponseModel> SaveCustomer(IdvCustomer model)
現(xiàn)在只有那些屬于 Administrator 組的人有訪問(wèn)權(quán)。角色可以是使用逗號(hào)分隔的列表,如使用“Administrator, ReportUser”來(lái)允許多個(gè)角色訪問(wèn)。
使用令牌訪問(wèn)安全端點(diǎn)
現(xiàn)在 API 已經(jīng)安全了,我們必須在每個(gè)請(qǐng)求中傳遞 Bearer 令牌來(lái)進(jìn)行驗(yàn)證。它看起來(lái)像這樣:

瞧,我現(xiàn)在可以訪問(wèn)管理員組保護(hù)的 POST 操作了。
這就完成了一個(gè)閉環(huán)...
總結(jié)
在最近的版本中,ASP.NET Core 中的身份驗(yàn)證和授權(quán)已經(jīng)變得簡(jiǎn)單了很多,但是要找到正確的文檔來(lái)設(shè)置 JWT 令牌身份驗(yàn)證的所有相關(guān)信息仍然不易。關(guān)于身份驗(yàn)證的信息很多,很容易在文檔中迷失方向,并最終可能選擇過(guò)時(shí)的信息,因?yàn)樵谡麄€(gè) ASP.NET Core 版本中,身份驗(yàn)證的行為已經(jīng)發(fā)生了重大變化。(基于本文)如果你要查找額外的信息,請(qǐng)確保它是 3.1 及以后的版本。
在這篇文章中,我已經(jīng)解決了 3.1 和 5.0 版本的問(wèn)題。值得慶幸的是,5.0 沒(méi)有看到對(duì)認(rèn)證/授權(quán) API 的進(jìn)一步破壞性改變。
通常情況下,我寫(xiě)下這篇文章是為了讓我自己安心,這樣我就能在一個(gè)地方得到所有的信息。希望你們中的一些人也會(huì)覺(jué)得這很有用。
【推薦】.NET Core開(kāi)發(fā)實(shí)戰(zhàn)視頻課程 ★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門(mén)篇-開(kāi)篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開(kāi)篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴(lài)注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫(xiě)出高性能的ASP.NET Core代碼
用abp vNext快速開(kāi)發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢(xún)優(yōu)化
