ASP.NET Core 2.0 中的令牌身份验证 - 完整指南
@@openiddict set TokenValidationParameters ValidateLifetime false Validate
ASP.NET Core 2.0 中的令牌身份验证 - 完整指南

令牌身份验证在过去几年一直是一个热门话题,尤其是在移动和 JavaScript 应用程序继续获得关注的情况下。OAuth 2.0 和 OpenID Connect等基于令牌的标准的广泛采用让更多的开发人员接触到令牌,但最佳实践并不总是很明确。
我在 ASP.NET Core 世界中花费了大量时间,并且从 1.0 之前的日子开始就一直在使用该框架。得益于内置的 JWT 验证中间件,ASP.NET Core 2.0 对使用和验证令牌提供了强大的支持。然而,许多人对从 ASP.NET 4 中删除令牌生成代码感到惊讶。在 ASP.NET Core 的早期,完整的令牌身份验证故事是一团混乱。
现在 ASP.NET Core 2.0(即将推出 2.1)稳定了,一切都安定下来了。在这篇文章中,我将研究令牌身份验证故事双方的最佳实践:令牌验证和令牌生成。
什么是令牌认证?
令牌身份验证是将令牌(有时称为访问令牌或不记名令牌)附加到 HTTP 请求以对其进行身份验证的过程。它通常与服务于移动或 SPA (JavaScript) 客户端的 API 一起使用。
检查到达 API 的每个请求。如果找到有效令牌,则允许该请求。如果没有找到令牌,或者令牌无效,请求将被拒绝并返回响应401 Unauthorized
。
令牌身份验证通常用于 OAuth 2.0 或 OpenID Connect 的上下文中。如果您想温习一下这些协议的工作原理,请阅读我们关于 OpenID Connect 的初级读物,或者在 YouTube 上观看我的OAuth 和 OpenID Connect 纯英语演讲!
在 ASP.NET Core 中验证令牌
JwtBearerAuthentication
得益于框架中包含的中间件,在 ASP.NET Core 中向 API 添加令牌身份验证非常容易。如果您正在使用由标准 OpenID Connect 服务器创建的令牌,则配置非常简单。
在您的Startup
类中,将中间件添加到ConfigureServices
方法中的任意位置,并使用授权服务器中的值对其进行配置:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "{yourAuthorizationServerAddress}";
options.Audience = "{yourAudience}";
});
然后,在您的Configure
方法中,将这一行添加到正上方UseMvc
:
app.UseAuthentication();
添加的第二步UseAuthentication()
很容易忘记!我已经做过几次了。如果您的经过身份验证的调用无法正常工作,请确保您已将此行添加到正确的位置(上方UseMvc
)。
中间件JwtBearer
在传入请求的 HTTP 授权标头中查找令牌(JSON Web 令牌或 JWT)。如果找到有效令牌,则请求被授权。然后,您将[Authorize]
属性添加到您想要保护的控制器或路由上:
[Route("/api/protected")
[Authorize]
public string Protected()
{
return "Only if you have a valid token!";
}
您可能想知道:仅指定权限和受众,中间件如何JwtBearer
验证传入的令牌?
自动授权服务器元数据
当JwtBearer
中间件第一次处理请求时,它会尝试从授权服务器(也称为授权机构或颁发者)检索一些元数据。此元数据或OpenID Connect 术语中的发现文档包含公钥和验证令牌所需的其他详细信息。(想知道元数据是什么样子的吗?这是一个发现文档示例。)
如果JwtBearer
中间件找到这个元数据文档,它会自动配置自己。漂亮漂亮!
如果文档不存在,您将收到错误消息:
System.IO.IOException: IDX10804: Unable to retrieve document from: "{yourAuthorizationServerAddress}".
System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).
如果您的授权服务器不发布此元数据,或者您只想自己指定令牌验证参数,则可以手动将它们添加到中间件配置中。
指定令牌验证参数
JwtBearer
如果您希望对令牌的验证方式进行细粒度控制,则可以使用全套选项:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// Clock skew compensates for server time drift.
// We recommend 5 minutes or less:
ClockSkew = TimeSpan.FromMinutes(5),
// Specify the key used to sign the token:
IssuerSigningKey = signingKey,
RequireSignedTokens = true,
// Ensure the token hasn't expired:
RequireExpirationTime = true,
ValidateLifetime = true,
// Ensure the token audience matches our audience value (default true):
ValidateAudience = true,
ValidAudience = "api://default",
// Ensure the token was issued by a trusted authorization server (default true):
ValidateIssuer = true,
ValidIssuer = "https://{yourOktaDomain}/oauth2/default"
};
});
最常见的设置选项TokenValidationParameters
是发行者、观众和时钟偏差。您还需要提供用于签署令牌的密钥,根据您使用的是对称密钥还是非对称密钥,这些密钥看起来会有所不同。
了解对称和非对称签名
您的授权服务器生成的令牌将使用对称密钥 (HS256) 或非对称密钥 (RS256) 进行签名。如果您的授权服务器发布发现文档,它将包含关键信息,因此您不必担心它是如何工作的。
但是,如果您自己配置中间件或手动验证令牌,则必须了解令牌的签名方式。对称密钥和非对称密钥有什么区别?
对称密钥
对称密钥,也称为共享密钥或共享秘密,是一个秘密值(如密码),同时保存在 API(您的应用程序)和颁发令牌的授权服务器上。授权服务器使用共享密钥对令牌负载进行签名,API 验证传入的令牌是否使用相同的密钥正确签名。
如果您有一个共享的对称密钥,可以很容易地将它与JwtBearer
中间件一起使用:
// For example only! Don't store your shared keys as strings in code.
// Use environment variables or the .NET Secret Manager instead.
var sharedKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("mysupers3cr3tsharedkey!"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// Specify the key used to sign the token:
IssuerSigningKey = sharedKey,
RequireSignedTokens = true,
// Other options...
};
});
确保您妥善保管钥匙!将它存储在您的代码中(如上面的示例)不是一个好主意,因为很容易不小心将其签入源代码管理。相反,将其存储在服务器上的环境变量中,或使用.NET Secret Manager。ASP.NET Core 配置模型可以轻松地从环境或用户机密中加载值:
var sharedKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration["SigningKey"]);
同样,不要将共享密钥存储在前端代码中或将其暴露给浏览器。它必须在您的服务器上受到保护。
非对称密钥
使用非对称签名,您无需在服务器上保留密钥。相反,使用公钥/私钥对:授权服务器使用秘密私钥签署令牌,并发布任何人都可以用来验证令牌的公钥。
通常,如上一节所述,公钥信息会自动从发现文档中检索。如果您需要手动指定它,则需要从授权服务器获取关键参数并创建一个SecurityKey
对象:
// Manually specify a public (asymmetric) key published as a JWK:
var publicJwk = new JsonWebKey
{
KeyId = "(some key ID)",
Alg = "RS256",
E = "AQAB",
N = "(a long string)",
Kty = "RSA",
Use = "sig"
};
在大多数情况下,公钥在授权服务器上的 JSON Web 密钥集 (JWKS) 中可用(这里是一个JWKS 示例)。授权服务器也可能会定期轮换密钥,因此您需要定期检查更新的密钥。如果你让JwtBearer
中间件通过发现文档自动配置,这一切都会自动运行!
在 ASP.NET Core 中手动验证令牌
在某些情况下,您可能需要在不使用JwtBearer
中间件的情况下验证令牌。使用中间件应该始终是首选,因为它可以很好地(自动)插入 ASP.NET Core 授权系统。
如果您绝对需要手动验证 JWT,则可以使用System.IdentityModel.Tokens.JwtJwtSecurityTokenHandler
包中的。它使用相同的类来指定验证选项:TokenValidationParameters
private static JwtSecurityToken ValidateAndDecode(string jwt, IEnumerable<SecurityKey> signingKeys)
{
var validationParameters = new TokenValidationParameters
{
// Clock skew compensates for server time drift.
// We recommend 5 minutes or less:
ClockSkew = TimeSpan.FromMinutes(5),
// Specify the key used to sign the token:
IssuerSigningKeys = signingKeys,
RequireSignedTokens = true,
// Ensure the token hasn't expired:
RequireExpirationTime = true,
ValidateLifetime = true,
// Ensure the token audience matches our audience value (default true):
ValidateAudience = true,
ValidAudience = "api://default",
// Ensure the token was issued by a trusted authorization server (default true):
ValidateIssuer = true,
ValidIssuer = "https://{yourOktaDomain}/oauth2/default"
};
try
{
var claimsPrincipal = new JwtSecurityTokenHandler()
.ValidateToken(jwt, validationParameters, out var rawValidatedToken);
return (JwtSecurityToken)rawValidatedToken;
// Or, you can return the ClaimsPrincipal
// (which has the JWT properties automatically mapped to .NET claims)
}
catch (SecurityTokenValidationException stvex)
{
// The token failed validation!
// TODO: Log it or display an error.
throw new Exception($"Token failed validation: {stvex.Message}");
}
catch (ArgumentException argex)
{
// The token was not well-formed or was invalid for some other reason.
// TODO: Log it or display an error.
throw new Exception($"Token was invalid: {argex.Message}");
}
}
如果您的授权服务器发布元数据文档,您可以使用Microsoft.IdentityModel.Protocols.OpenIdConnectOpenIdConnectConfigurationRetriever
包中的类检索它。这将使您自动获得签名密钥:
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
// .well-known/oauth-authorization-server or .well-known/openid-configuration
"{yourAuthorizationServerAddress}/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever());
var discoveryDocument = await configurationManager.GetConfigurationAsync();
var signingKeys = discoveryDocument.SigningKeys;
这会处理令牌身份验证的验证方面,但是如何生成令牌本身呢?
在 ASP.NET Core 中生成用于身份验证的令牌
回到 ASP.NET 4.5 时代,UseOAuthAuthorizationServer
中间件为您提供了一个端点,可以轻松地为您的应用程序生成令牌。但是,ASP.NET Core 团队决定不将它引入 ASP.NET Core,这意味着您需要插入其他东西。具体来说,您需要找到或构建一个可以生成令牌的授权服务器.
获取授权服务器的两种常见方式是:
- 使用 Azure AD B2C 或Okta等云服务
- 构建或配置您自己的
使用 Okta 的托管授权服务器
托管授权服务器是生成令牌的最简单方法,因为您不需要自己构建(或维护)任何东西。您可以注册一个免费帐户,然后按照Okta + ASP.NET Core API 快速入门获取分步说明。
由于 Okta 为您创建的授权服务器具有标准的发现文档,因此JwtBearer
配置非常简单:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://{yourOktaDomain}/oauth2/default";
options.Audience = "api://default";
});
如果你想推出自己的授权服务器,你可以使用流行的社区构建包之一:
OpenIddict
OpenIddict是一个易于配置的授权服务器,可以很好地与ASP.NET Core Identity和 Entity Framework Core 配合使用。它直接插入 ASP.NET Core 中间件管道并且易于配置。
如果您已经在使用 ASP.NET Core Identity 并希望为您的用户生成令牌,OpenIddict 是一个不错的选择。您可以按照 Mike Rousos在 MSDN 博客上的深入教程来设置它并在您的应用程序中进行配置。
ASOS
AspNet.Security.OpenIdConnect.Server包的级别低于 OpenIddict(实际上,OpenIddict 在底层使用它)。它需要更多的设置工作,但当您想要更直接地控制 OpenID Connect 协议的处理方式和令牌的生成方式时,它会很有用。Kévin Chalet 在他的博客上有一个关于创建 OpenID Connect 服务器的深入教程。
身份服务器4
Thinktecture 的开源IdentityServer 项目已经存在很长时间了,它通过 IdentityServer4 获得了针对 .NET Core 的重大更新。在这里讨论的三个包中,它是最强大和最灵活的。
当您想要推出自己的成熟的 OpenID Connect 授权服务器来处理联合和单点登录等复杂用例时,IdentityServer 是一个不错的选择。根据您的用例,配置 IdentityServer4 可能会有点复杂。幸运的是,官方文档涵盖了很多常见的场景。
令牌认证可能很复杂!
我希望这篇文章能帮助它感觉不那么混乱。ASP.NET Core 团队在简化向 ASP.NET Core API 添加令牌身份验证方面做得非常出色,并且 OpenIddict 和 Okta 等选项可以轻松启动为您的客户端生成令牌的授权服务器。
如果您想继续学习,这里有更多资源:
- Okta 博客上的如何使用令牌身份验证保护您的 .NET Web API
- 在 Andrew Lock 的博客上深入了解 JWT 承载中间件
- OAuth 到底是什么?
- 我们的OpenID Connect 入门
- OAuth 和 OpenID Connect 在 YouTube 上以简单的英语呈现,真正属于您
我很想听听您的反馈!如果您有任何问题或想法,请在下方发表评论。您也可以通过 Twitter @oktadev联系我们。
https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide
---> https://devblogs.microsoft.com/dotnet/bearer-token-authentication-in-asp-net-core/
ASP.NET Core 中的不记名令牌身份验证
这是 Mike Rousos 的客座帖子
介绍
ASP.NET Core Identity自动支持 cookie 身份验证。使用Google、Facebook或Twitter ASP.NET Core 身份验证包支持外部提供商的身份验证也很简单。不过,需要做更多工作的一种身份验证方案是通过不记名令牌进行身份验证。我最近与一位客户合作,他有兴趣在使用 ASP.NET Core 后端的移动应用程序中使用 JWT 不记名令牌进行身份验证。由于他们的一些客户没有可靠的互联网连接,他们还希望能够在无需与发行服务器通信的情况下验证令牌。
在本文中,我简要介绍了如何在 ASP.NET Core 中发布 JWT 不记名令牌。在后续博文中,我将展示如何使用这些相同的令牌进行身份验证和授权(即使无法访问身份验证服务器或身份数据存储)。
离线令牌验证注意事项
客户有一个本地服务器,其中包含需要由客户端设备定期访问和更新的业务信息。与其在本地存储用户名和散列密码,客户更喜欢使用托管在 Azure 中的通用身份验证微服务,并在除此特定场景之外的许多场景中使用。不过,这个特定场景很有趣,因为客户位置(服务器和客户端所在的位置)与 Internet 之间的连接并不可靠。因此,他们希望用户能够在早上某个时间连接建立时进行身份验证,并拥有一个在该用户的整个工作班次期间都有效的令牌。因此,本地服务器需要能够在不访问 Azure 身份验证服务的情况下验证令牌。
使用JWT 令牌可以轻松完成此本地验证。JWT 令牌通常包含一个主体,其中包含有关经过身份验证的用户(主题标识符、声明等)、令牌的颁发者、令牌的目标受众(接收者)以及到期时间(令牌在该时间之后无效的)。该令牌还包含RFC 7518中详述的加密签名. 此签名由只有身份验证服务器知道的私钥生成,但可以由拥有相应公钥的任何人进行验证。一个 JWT 验证工作流程(由 AD 和一些身份提供者使用)涉及从发布服务器请求公钥并使用它来验证令牌的签名。不过,在我们的离线场景中,本地服务器可以提前准备好必要的公钥。这种架构的挑战在于,只要云服务使用的私钥发生变化,本地服务器就需要获得更新的公钥,但这种不便意味着在验证 JWT 令牌时不需要互联网连接。
颁发身份验证令牌
如前所述,Microsoft.AspNetCore.* 库不支持发布 JWT 令牌。但是,还有其他几个不错的选择。
首先,Azure Active Directory 身份验证将身份和身份验证作为服务提供。使用 Azure AD 是一种在 ASP.NET Core 应用程序中获取身份的快速方法,而无需编写身份验证服务器代码。
或者,如果开发人员希望自己编写身份验证服务,则可以使用几个第三方库来处理这种情况。IdentityServer4是一个用于 ASP.NET Core 的灵活的 OpenID Connect 框架。另一个不错的选择是OpenIddict。与 IdentityServer4 一样,OpenIddict 为 ASP.NET Core 提供 OpenID Connect 服务器功能。OpenIddict 和 IdentityServer4 都可以很好地与 ASP.NET Identity 3 配合使用。
对于这个演示,我将使用 OpenIddict。IdentityServer4 文档中提供了有关使用 IdentityServer4 完成相同任务的优秀文档,我也鼓励您看一看。
免责声明
请注意,IdentityServer4 和 OpenIddict 目前都是预发布包。OpenIddict 目前作为 beta 版本发布,IdentityServer4 作为 RC 版本发布,因此两者仍在开发中并可能发生变化!
设置用户存储
在这种情况下,我们将使用一个通用的基于 ASP.NET Identity 3 的用户存储,通过 Entity Framework Core 访问。因为这是一个常见的场景,所以设置它就像从新项目模板创建一个新的 ASP.NET Core Web 应用程序并为身份验证模式选择“个人用户帐户”一样简单。
此模板将提供默认ApplicationUser
类型和 Entity Framework Core 连接来管理用户。中的连接字符串可以是修饰符以指向要存储此数据的数据库。appsettings.json
因为 JWT 令牌可以封装声明,所以除了默认的用户名或电子邮件地址之外,还可以包含一些用户声明很有趣。出于演示目的,让我们包含两种不同类型的声明。
添加角色
ASP.NET Identity 3 包括角色的概念。为了利用这一点,我们需要创建一些可以分配给用户的角色。在实际应用程序中,这可能通过 Web 界面管理角色来完成。不过,对于这个简短的示例,我只是通过将以下代码添加到以下示例角色来为数据库播种:startup.cs
// 初始化一些测试角色。在现实世界中,这些将由角色管理器显式设置private string [] roles = new [] { " User " , " Manager " , " Administrator " }; private async Task InitializeRoles ( RoleManager < IdentityRole > roleManager ) { foreach ( var role in roles ) { if (! await
角色经理。RoleExistsAsync (角色)) { var newRole = new IdentityRole (角色); 等待角色管理器。创建异步(新角色);// 在现实世界中,可能存在与角色关联的声明// _roleManager.AddClaimAsync(newRole, new ) } } }
然后我InitializeRoles
从我的应用程序的方法中调用。IoC 可以检索所需的参数(只需向您的方法添加一个参数)。Startup.Configure
RoleManager
InitializeRoles
RoleManager
Startup.Configure
因为角色已经是 ASP.NET Identity 的一部分,所以不需要修改模型或我们的数据库架构。
向数据模型添加自定义声明
也可以在 JWT 令牌中对完全自定义的声明进行编码。为了证明这一点,我向我的ApplicationUser
类型添加了一个额外的属性。出于示例目的,我添加了一个名为的整数OfficeNumber
:
公共虚拟int OfficeNumber {得到; 设置;}
这在现实世界中可能不是一个有用的声明,但我将它特别添加到我的示例中,因为它不是我们正在使用的任何框架已经处理的那种声明。
我还更新了与创建新用户相关的视图模型和控制器,以允许在创建新用户时指定角色和办公室号码。
我向类型添加了以下属性RegisterViewModel
:
[ Display ( Name = " Is administrator " )] public bool IsAdministrator { get ; 设置;}
[ Display ( Name = " Is manager " )] public bool IsManager { get ; 设置;}
[必需] [ Display ( Name = " Office Number " )] public int OfficeNumber { get ; 设置;}
我还在注册视图中添加了用于收集此信息的 cshtml:
< div class = " form-group " > < label asp-for = " OfficeNumber " class = " col-md-2 control-label " ></label> < div class = " col - md-10 " > < input asp-for = " OfficeNumber " class = " form-control " /> < span asp-validation-for = "办公室号码
" class = " text -danger " > </span> </div> </div><div class = " form -group " >< label asp-for = " IsManager " class = " col -md- 2 control- label " ></ label >< div class = " col-md-10 " ><输入asp-for = " IsManager
" class = " form -control " / >< span asp - validation-for =" IsManager " class = " text -danger " ></span> </div> </div>