ASP.NET Core 2.0 中的令牌身份验证 - 完整指南

@@openiddict set TokenValidationParameters ValidateLifetime false Validate

 

ASP.NET Core 2.0 中的令牌身份验证 - 完整指南

 
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 ManagerASP.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,这意味着您需要插入其他东西。具体来说,您需要找到或构建一个可以生成令牌的授权服务器.

获取授权服务器的两种常见方式是:

使用 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 等选项可以轻松启动为您的客户端生成令牌的授权服务器。

如果您想继续学习,这里有更多资源:

我很想听听您的反馈!如果您有任何问题或想法,请在下方发表评论。您也可以通过 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 身份验证。使用GoogleFacebookTwitter 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.ConfigureRoleManagerInitializeRolesRoleManagerStartup.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> <div class = " form - group " >< label asp-for = " IsAdministrator " class = " col-md-2 control-label " ></标签>  
          
    

 
      
    < div class = " col-md-10 " > < input asp-for = " IsAdministrator " class = " form-control " /> < span asp-validation-for = " IsAdministrator " class = " text-danger " >< /跨度> </ div > </ div >

最后,我更新了在数据库中创建用户时设置角色和办公室号码信息的操作。请注意,我们为办公室号码添加了自定义声明。这利用了 ASP.NET Identity 的自定义索赔跟踪。请注意,ASP.NET Identity 不存储声明值类型,因此即使在声明始终为整数的情况下(如本例所示),它也将作为字符串存储和返回。在这篇文章的后面,我将解释如何将非字符串声明包含在 JWT 令牌中。AccountController.Register

var user = new ApplicationUser {用户名=模型电子邮件电子邮件=模型电子邮件OfficeNumber =型号办公室号码}; var结果=等待_userManager CreateAsync 用户模型密码);如果结果成功{如果模型          
 
 

     IsAdministrator ) {等待_userManager AddToRoleAsync 用户管理员);} else if ( model.IsManager ) { await _userManager . _ _ AddToRoleAsync 用户经理);}
    
         
    
      
    
         
    

    var officeClaim = new Claim ( " office " , user.OfficeNumber.ToString ( ) , ClaimValueTypes.Integer ) ; _ _ _ 等待_userManager AddClaimAsync (用户, officeClaim ); ...

更新数据库架构

进行这些更改后,我们可以使用 Entity Framework 的迁移工具轻松更新数据库以匹配(对数据库的唯一更改应该是OfficeNumber向用户表添加一列)。要迁移,只需从命令行运行即可。dotnet ef migrations add OfficeNumberMigrationdotnet ef database update

此时,认证服务器应该允许注册新用户。如果您按照代码进行操作,请继续并在此时添加一些示例用户。

使用 OpenIddict 发行代币

OpenIddict包仍处于预发布阶段,因此在 NuGet.org尚不可用相反,该包在aspnet-contrib MyGet feed上可用

要恢复它,我们需要将该提要添加到我们解决方案的 NuGet.config 中。如果您的解决方案中还没有 NuGet.config 文件,您可以添加一个如下所示的文件:

<? xml version = " 1.0 " encoding = " utf-8 " ?> < configuration > < packageSources > < add key = " nuget.org " value = " https://api.nuget.org/v3/index.json " / > <添加=  aspnet-contrib =  https://www.myget.org/F/aspnet-contrib/api/v3/index.json  /> <

  
       
       
  > </配置>

完成后,在文件的依赖项部分添加对和 的引用。OpenIddict.Mvc 包含一些有用的扩展,允许 OpenIddict 自动将 OpenID Connect 请求绑定到 MVC 操作参数。"OpenIddict": "1.0.0-beta1-""OpenIddict.Mvc": "1.0.0-beta1-"project.json

启用 OpenIddict 端点只需要几个步骤。

使用 OpenIddict 模型类型

第一个更改是更新您的ApplicationDBContext模型类型以继承自OpenIddictDbContext而不是IdentityDbContext.

进行此更改后,迁移数据库以更新它,以及 ()。dotnet ef migrations add OpenIddictMigrationdotnet ef database update

配置 OpenIddict

接下来,有必要在我们的类型中ConfigureServices的方法中注册 OpenIddict 类型Startup这可以通过这样的调用来完成:

服务AddOpenIddict < ApplicationDbContext >() AddMvcBinders () EnableTokenEndpoint  /connect/token 使用 JsonWebTokens () 允许密码流()AddSigningCertificate ( jwtSigningCert );

此处调用的具体方法OpenIddictBuilder很重要,需要理解。

  • 添加 MvcBinders。此方法注册自定义模型绑定器,这些绑定器将OpenIdConnectRequest使用从传入 HTTP 请求的上下文中读取的 OpenID Connect 请求填充 MVC 操作中的参数。这不是必需的,因为可以手动读取 OpenID Connect 请求,但这是一个有用的便利。
  • 启用令牌端点。此方法允许您指定将提供身份验证令牌的端点。上面显示的端点 (/connect/token) 是令牌发行的一个非常常见的默认端点。OpenIddict 需要知道此端点的位置,以便在客户端查询有关如何连接的信息(使用 .well-known/openid-configuration 端点,OpenIddict 自动提供)时将其包含在响应中。OpenIddict 还将验证对此端点的请求,以确保它们是有效的 OpenID Connect 请求。如果请求无效(例如缺少强制参数grant_type,例如 ),则 OpenIddict 甚至会在请求到达应用程序的控制器之前拒绝该请求。
  • 使用 JsonWebTokens。这指示 OpenIddict 使用 JWT 作为其生成的不记名令牌的格式。
  • 允许密码流。这会在用户登录时启用密码授予类型。不同的 OpenID Connect 授权流程记录在RFCOpenID Connect规范中。密码流意味着客户端授权是根据客户端提供的用户凭据(名称和密码)执行的。这是最符合我们示例场景的流程。
  • 添加签名证书。此 API 指定应用于签署 JWT 令牌的证书。在我的示例代码中,我jwtSigningCert从磁盘 ( ) 上的 pfx 文件生成参数在真实场景中,证书更有可能从身份验证服务器的证书存储中加载,在这种情况下,将使用不同的重载(使用证书的指纹和存储名称/位置)。 var jwtSigningCert = new X509Certificate2(certLocation, certPassword);AddSigningCertificate
    • makecert如果您出于测试目的需要自签名证书,可以使用和pvk2pfx命令行工具(应该位于 Visual Studio 开发人员命令提示符的路径上) 生成一个。
      • makecert -"CN=AuthSample" -a sha256 -sv AuthSample.pvk -AuthSample.cer这将创建一个新的自签名测试证书,其公钥位于 AuthSample.cer 中,私钥位于 AuthSample.pvk 中。
      • pvk2pfx -pvk AuthSample.pvk -spc AuthSample.cer -pfx AuthSample.pfx -pi [A password]这会将 pvk 和 cer 文件合并到一个 pfx 文件中,其中包含证书的公钥和私钥(受密码保护)。
      • 这个 pfx 文件是 OpenIddict 需要加载的文件(因为私钥是签署令牌所必需的)。请注意,此私钥(以及包含它的任何文件)必须保持安全。
  • 禁用 Https 要求。上面的代码片段不包括对 的调用,但这样的调用在测试期间可能很有用,可以禁用通过 HTTPS 进行身份验证调用的要求。当然,这绝不应该在测试之外使用,因为它会允许在传输过程中观察到身份验证令牌,从而使恶意方能够冒充合法用户。DisableHttpsRequirement()

启用 OpenIddict 端点

一旦AddOpenIddict用于配置 OpenIddict 服务,应添加对(应在现有调用之后进行)的调用,以在应用程序的 HTTP 请求处理管道中实际启用 OpenIddict。app.UseOpenIddict();UseIdentityStartup.Configure

实施连接/令牌端点

启用身份验证服务器所需的最后一步是实现连接/令牌端点。在 OpenIddict 配置期间进行的调用EnableTokenEndpoint指示令牌颁发端点的位置(并允许 OpenIddict 验证传入的 OIDC 请求),但端点仍需要实现。

OpenIddict 的所有者 Kévin Chalet 在此示例中提供了一个很好的示例,说明如何实现支持密码流的令牌端点我在这里重申了如何创建简单令牌端点的要点。

首先,创建一个名为的新控制器ConnectController并给它一个Token后期动作。当然,具体名称并不重要,重要的路由与给 EnableTokenEndpoint 的名称相匹配。

给动作方法一个OpenIdConnectRequest参数。因为我们使用的是 OpenIddict MVC 绑定器,所以此参数将由 OpenIddict 提供。或者(不使用 OpenIddict 模型联编程序),GetOpenIdConnectRequest扩展方法可用于检索 OpenID Connect 请求。

根据请求的内容,您应该验证请求是否有效。

  1. 确认授权类型符合预期(此身份验证服务器的“密码”)。
  2. 确认请求的用户存在(使用 ASP.NET Identity UserManager)。
  3. 确认请求的用户能够登录(因为 ASP.NET Identity 允许锁定或尚未确认的帐户)。
  4. 确认提供的密码正确(再次使用UserManager)。

如果请求中的所有内容都已通过,则ClaimsPrincipal可以使用.SignInManager.CreateUserPrincipalAsync

ASP.NET 身份已知的角色和自定义声明将自动出现在ClaimsPrincipal如果需要对索赔进行任何更改,现在就可以进行。

一组重要的索赔更新是将目的地附加到索赔。如果声明包含该令牌类型的目的地,则该声明仅包含在令牌中。因此,即使ClaimsPrincipal将包含所有 ASP.NET Identity 声明,它们也只有在具有适当的目的地时才会包含在令牌中。这允许一些声明保持私有,而其他声明仅包含在特定令牌类型(访问或身份令牌)中,或者如果请求特定范围。出于这个简单演示的目的,我包括了所有令牌类型的所有声明。

这也是向ClaimsPrincipal通常,使用 ASP.NET Identity 跟踪声明就足够了,但如前所述,ASP.NET Identity 不会记住声明值类型。因此,如果 office 声明为整数(而不是字符串)很重要,我们可以根据ApplicationUserUserManager不能直接向 a 添加声明ClaimsPrincipal,但可以检索和修改基础标识。例如,如果 office claim 是在这里创建的(而不是在用户注册时),它可以这样添加:

var identity = ( ClaimsIdentity )主体身份var officeClaim = new Claim ( " office " , user.OfficeNumber.ToString ( ) , ClaimValueTypes.Integer ) ; _ _ _ 
办公室索赔SetDestinations ( OpenIdConnectConstants.Destinations.AccessToken , OpenIdConnectConstants.Destinations.IdentityToken _ _ _ _ _ _ _ _ 
    ); 
身份AddClaim ( officeClaim );

最后,AuthenticationTicket可以从 claims principal 创建 an 并用于登录用户。票证对象允许我们使用有用的 OpenID Connect 扩展方法来指定要授予访问权限的范围和资源。在我的示例中,我传递了由服务器能够提供的范围过滤的请求范围。对于资源,我提供了一个硬编码字符串,指示应该使用此令牌访问的资源。在更复杂的情况下,在确定票证中包含哪些资源声明时,可能会考虑请求的资源 ( )。请注意,根据JWT 规范,资源(映射到 JWT 的受众元素)不是强制性的,尽管许多 JWT 消费者期望它们。request.GetResources()

综上所述,这是连接/令牌端点的简单实现:

[ HttpPost ] public async Task < IActionResult > Token ( OpenIdConnectRequest request ) { if (! request . IsPasswordGrantType ()) { // 如果请求不是密码授予类型,则返回错误请求return BadRequest ( new OpenIdConnectResponse { Error = OpenIdConnectConstants . Errors . UnsupportedGrantType , ErrorDescription = "
   

     
    
        
          
        
              
              不支持指定的授权类型。" }); }
        
    

    var用户=等待_userManager FindByNameAsync 请求用户名);if ( user == null ) { //如果用户存在返回错误请求_ _ _ _ _ _ _ _ _ _ _ } 
      
    
        
           
        
              
              
        
    

    // 检查用户是否可以登录并且没有被锁定。//如果支持因素身份验证检查是否用户启用2FA也是合适_ _ _ _ _ { // 返回错误请求是用户无法登录return BadRequest ( new OpenIdConnectResponse { Error =
    
         
    
        
          
        
              OpenIdConnectConstants 错误InvalidGrant , ErrorDescription = "指定用户无法登录。" }); }
              
        
    

    if ( ! await _userManager.CheckPasswordAsync ( user , request.Password ) ) { //如果密码无效返回错误请求_ _ _ _ _ _ _ _ _ _ _ _ _ _ ; } 
    
        
          
        
              
              
        
    

    // 用户现已通过验证,因此如有必要,请重置锁定计数if ( _userManager . SupportsUserLockout ) { await _userManager . ResetAccessFailedCountAsync 用户);}
     
    
        
    

    // 创建主体var principal = await _signInManager . CreateUserPrincipalAsync 用户);
     

    // 默认情况下,声明不会与特定目的地相关联,因此我们必须指明它们是否应该// 包含在访问和身份令牌中。foreach ( var claim in principal . Claims ) { // 对于此示例,仅包括所有令牌类型中的所有声明。// 实际上,声明的目的地可能因令牌类型和请求的范围而异。
        索赔SetDestinations ( OpenIdConnectConstants.Destinations.AccessToken , OpenIdConnectConstants.Destinations . _ _ _ _ _ _
    
     
    
        
         身份令牌);}
    

    // 为用户的主体创建一个新的身份验证票var ticket = new AuthenticationTicket ( 
        principal , new AuthenticationProperties (), OpenIdConnectServerDefaults . AuthenticationScheme );
      
         
        

    // 酌情包括资源和范围var scope = new [] { OpenIdConnectConstants . 范围OpenId , OpenIdConnectConstants 范围电子邮件OpenIdConnectConstants 范围配置文件OpenIdConnectConstants 范围离线访问OpenIddictConstants 范围角色}。相交请求。GetScopes ())
     
    
        
        
        
        
        
    
SetResources ( " http://localhost:5000/ " ); 设置范围作用域);

    //登录用户return SignIn ( ticket.Principal , ticket.Properties , ticket.AuthenticationScheme ) ; _ _ _ }

测试认证服务器

此时,我们的简单身份验证服务器已完成,应该可以为我们数据库中的用户颁发 JWT 不记名令牌。

OpenIddict 实现了 OpenID Connect,因此我们的示例应该支持一个标准端点,其中包含有关如何向服务器进行身份验证的信息。/.well-known/openid-configuration

如果您按照构建示例的步骤进行操作,请启动应用程序并导航到该端点。你应该得到一个类似这样的 json 响应:

{ 发行人 http://localhost:5000/  jwks_uri  http://localhost:5000/.well-known/jwks  token_endpoint  http://localhost:5000/ connect/token  code_challenge_methods_supported [  S256  ], grant_types_supported [  password  ],subject_types_supported 
   
   
   
     
     
   [  public  ], scopes_supported [  openid  profile  email  phone  roles  ], id_token_signing_alg_values_supported [  RS256  ] }

这为客户提供了有关我们的身份验证服务器的信息。一些有趣的值包括:

  • jwks_uri属性是客户端可用于检索公钥以验证来自发行者的令牌签名的端点。
  • token_endpoint给出应用于身份验证请求的端点。
  • grant_types_supported属性是服务器支持的授权类型列表。在此示例的情况下,这只是password.
  • scopes_supported是客户端可以请求访问的范围的列表。

如果您想检查是否使用了正确的证书,您可以导航到端点jwks_uri以查看服务器使用的公钥。x5t响应的属性应该是证书指纹您可以根据您希望使用的证书的指纹进行检查,以确认它们是相同的。

最后,我们可以通过尝试登录来测试身份验证服务器!这是通过 POST 到token_endpoint您可以使用像Postman这样的工具来组合测试请求。帖子的地址应该是token_endpointURI,帖子的正文应该是 x-www-form-urlencoded 并且包括以下项目:

  • 对于这种情况, grant_type必须是“密码”。
  • 用户名应该是登录的用户名。
  • password应该是用户的密码。
  • 范围应该是访问所需的范围。
  • resource是一个可选参数,它可以指定令牌要访问的资源。使用它可以帮助确保为访问一种资源而颁发的令牌不会被重新用于访问不同的资源。

以下是我测试连接/令牌 API 的完整请求和响应:

要求

POST /connect/token HTTP/1.1
Host: localhost:5000
Cache-Control: no-cache
Postman-Token: f1bb8681-a963-2282-bc94-03fdaea5da78
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=Mike%40Fabrikam.com&password=MikePassword1!&scope=openid+email+name+profile+roles

回复

{  token_type  Bearer  access_token 
   
   eyJhbGciOiJSUzI1NiIsImtpZCI6IkU1N0RBRTRBMzU5NDhGODhBQTg2NThFQkExMUZFOUIxMkI5Qzk5NjIiLCJ0eXAiOiJKV1QifQ..q-c6Ld1b7c77td8B-0LcppUbL4a8JvObiru4FDQWrJ_DZ4_zKn6_0ud7BSijj4CV3d3tseEM-3WHgxjjz0e8aa4Axm55y4Utf6kkjGjuYyen7bl9TpeObnG81ied9NFJTy5HGYW4 ysq4DkB2IEOFu4031rcQsUonM1chtz14mr3wWHohCi7NJY0APVPnCoc6ae4bivqxcYxbXlTN4p6bfBQhr71kZzP0AU_BlGHJ1N8k4GpijHVz2lT-2ahYaVSvaWtqjlqLfM_8uphNH3V7T 7smaMpomQvA6u-CTZNJOZKalx99GNL4JwGk13MlikdaMFXhcPiamhnKtfQEsoNauA" , " expires_in " : 1800 }

access_token是 JWT,无非是一个由三部分组成的 base64 编码字符串([header].[body].[signature])。许多网站提供JWT解码功能

上面的访问令牌具有以下内容:

{  alg  RS256  kid  E57DAE4A35948F88AA8658EBA11FE9B12B9C9962  typ  JWT  }。{  unique_name  Mike@Contoso.com  AspNet.Identity.SecurityStamp  c33e8749-1280-4d99-9931-253953674132 角色
   
   
   


   
   
   
   office  300  jti  ce09ee0e-5d12-46e2-bade-252a6a0f7a0e  usage  access_token  scope [ email  profile  roles  ], c035be99-2247-4769-9dcd-54bdadeef601  
   
   
   
    
    
    
  
   
    http://localhost:5001/  nbf  1476997029 exp  1476998829 iat  1476997029 iss  http://localhost:5000/  }。[签名]

令牌中的重要字段包括:

  • kid是密钥 ID,可用于查找验证令牌签名所需的密钥。
    • 同样, x5t是签名证书的指纹。
  • roleoffice捕获我们的自定义声明。
  • exp是令牌应该过期并且不再被视为有效的时间戳。
  • iss是发行服务器的地址。

这些字段可用于验证令牌。

结论和后续步骤

希望本文提供了有关 ASP.NET Core 应用程序如何发布 JWT 不记名令牌的有用概述。使用 cookie 或第三方社交提供商进行身份验证的 in-box 能力对于许多场景来说已经足够了,但在其他情况下(尤其是在支持移动客户端时),不记名身份验证更为方便。

寻找即将发布的这篇文章的后续文章,内容包括如何在 ASP.NET Core 中验证令牌,以便它可以用于自动验证和登录用户。为了与我与客户遇到的原始场景保持一致,我们将确保无需访问身份验证服务器或身份数据库即可完成所有验证。

资源

 
 
posted @   dreamw  阅读(272)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2021-05-26 通过Dapr实现一个简单的基于.net的微服务电商系统
点击右上角即可分享
微信分享提示