ASP.NET Core中的基于令牌的身份验证(更新版)

69

我正在使用ASP.NET Core应用程序。我正在尝试实现基于令牌的身份验证,但无法弄清如何使用新的安全系统

我的情景:客户端请求令牌。我的服务器应该授权用户并返回访问令牌,该令牌将由客户端在后续请求中使用。

这里有两篇关于实现我所需内容的好文章:

问题是-对我来说,在ASP.NET Core中如何做到这一点并不明显。

我的问题是:如何配置ASP.NET Core Web Api应用程序以使用基于令牌的身份验证?我应该追求哪个方向?您是否撰写过关于最新版本的文章,或者知道可以找到哪些文章?

谢谢!


6
请投票重新开放此问题,因为与之相连的重复问题现在不能回答这个问题。自四月份以来,MVC6由于命名空间的更改已经与现在大不相同。此外,那个问题中给出的答案在通过JWT生成令牌和通过JWT消耗令牌的示例中没有提供足够的细节。 - Adam
https://dev59.com/YKjja4cB1Zd3GeqP5A4k#54206566 - turgayozgur
5个回答

73

我从Matt Dekrey的精彩答案中得到灵感,创建了一个完全可用的基于令牌的身份验证示例,可用于与ASP.NET Core(1.0.1)配合使用。您可以在此GitHub存储库中找到完整代码(还有针对1.0.0-rc1beta8beta7 ),但简而言之,重要步骤如下:

为您的应用程序生成密钥

在我的示例中,我每次启动应用程序时都会生成一个随机密钥。您需要生成并存储某个密钥,并将其提供给您的应用程序。请查看此文件以了解如何生成随机密钥以及如何从.json文件中导入。如@kspearrin的评论所建议的那样,Data Protection API似乎是管理密钥“正确”的理想选择,但我尚未弄清楚是否可能。如果您想出来,请提交一个拉取请求!

Startup.cs - ConfigureServices

在这里,我们需要加载用于签署令牌的私钥,我们还将使用它来验证表示的令牌。我们将密钥存储在类级别变量key中,并在下面的Configure方法中重复使用它。TokenAuthOptions是一个简单的类,其中包含了我们在TokenController中创建密钥所需的签名标识、受众和发行者。

// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

// Create the key, and a set of token options to record signing credentials 
// using that key, along with the other parameters we will need in the 
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
    Audience = TokenAudience,
    Issuer = TokenIssuer,
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};

// Save the token options into an instance so they're accessible to the 
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);

// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
        .RequireAuthenticatedUser().Build());
});

我们还设置了授权策略,以便在我们希望保护的端点和类上使用[Authorize("Bearer")]

Startup.cs - Configure

在这里,我们需要配置JwtBearerAuthentication:

app.UseJwtBearerAuthentication(new JwtBearerOptions {
    TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = key,
        ValidAudience = tokenOptions.Audience,
        ValidIssuer = tokenOptions.Issuer,

        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,

        // This defines the maximum allowable clock skew - i.e.
        // provides a tolerance on the token expiry time 
        // when validating the lifetime. As we're creating the tokens 
        // locally and validating them on the same machines which 
        // should have synchronised time, this can be set to zero. 
        // Where external tokens are used, some leeway here could be 
        // useful.
        ClockSkew = TimeSpan.FromMinutes(0)
    }
});

TokenController

在令牌控制器中,您需要拥有一种使用在Startup.cs中加载的密钥生成签名密钥的方法。我们已经在Startup中注册了一个TokenAuthOptions实例,因此我们需要在TokenController的构造函数中注入它:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

然后您需要在登录端点的处理程序中生成令牌,在我的示例中,我正在使用 if 语句获取用户名和密码并进行验证,但您需要做的关键是创建或加载基于声明的标识,并为其生成令牌:

public class AuthRequest
{
    public string username { get; set; }
    public string password { get; set; }
}

/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
    // Obviously, at this point you need to validate the username and password against whatever system you wish.
    if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
    {
        DateTime? expires = DateTime.UtcNow.AddMinutes(2);
        var token = GetToken(req.username, expires);
        return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
    }
    return new { authenticated = false };
}

private string GetToken(string user, DateTime? expires)
{
    var handler = new JwtSecurityTokenHandler();

    // Here, you should create or look up an identity for the user which is being authenticated.
    // For now, just creating a simple generic identity.
    ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });

    var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
        Issuer = tokenOptions.Issuer,
        Audience = tokenOptions.Audience,
        SigningCredentials = tokenOptions.SigningCredentials,
        Subject = identity,
        Expires = expires
    });
    return handler.WriteToken(securityToken);
}

就是这样了。只需将 [Authorize("Bearer")] 添加到您想要保护的任何方法或类中,如果尝试在没有token的情况下访问,则应该会收到错误提示。如果您想返回401而不是500错误,则需要注册自定义异常处理程序 就像我在这里的示例中所做的那样


2
有没有可能只添加 [Authorize] 而不是 [Authorize("Bearer")]? - zoranpro
2
我相信这会起作用@zoranpro——只要您在startup.cs中注册了一个身份验证中间件。如果您注册了多个,则[Authorize]将允许通过任何这些方法进行身份验证的人访问-根据您的使用情况,这可能是可以接受的。 - Mark Hughes
3
好的,我找到了:Header 名称应该是:“Authorization”,值为:“Bearer [token]”。 - Piotrek
4
在浏览互联网及其相关网站寻找答案后,只有这个回答适用于 ASP.NET 5 RC!非常感谢 @MarkHughes,你应该真的为这个答案撰写自己的问答,并提供你优秀的示例。 - Thomas Hagström
2
@MarkHughes 请更新至RC2,因为您的UseJwtBearerAuthentication语法已不再适用。 - Camilo Terevinto
显示剩余4条评论

24

这实际上是我另一个回答的重复,链接在这里:https://dev59.com/cl4b5IYBdhLWcg3wfxxI#29698502。由于那个回答更受关注,我会保持它更加更新。那里的评论也可能对你有用!

针对 .Net Core 2 更新:

此前版本的回答使用了 RSA;但是,如果生成令牌的代码也是验证令牌的代码,那么实际上并不需要使用 RSA。然而,如果你想分配责任,你可能仍然希望使用 Microsoft.IdentityModel.Tokens.RsaSecurityKey 的实例来完成这项任务。

  1. Create a few constants that we'll be using later; here's what I did:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    
  2. Add this to your Startup.cs's ConfigureServices. We'll use dependency injection later to access these settings. I'm assuming that your authenticationConfiguration is a ConfigurationSection or Configuration object such that you can have a different config for debug and production. Make sure you store your key securely! It can be any string.

    var keySecret = authenticationConfiguration["JwtSigningKey"];
    var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret));
    
    services.AddTransient(_ => new JwtSignInHandler(symmetricKey));
    
    services.AddAuthentication(options =>
    {
        // This causes the default authentication scheme to be JWT.
        // Without this, the Authorization header is not checked and
        // you'll get no results. However, this also means that if
        // you're already using cookies in your app, they won't be 
        // checked by default.
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters.ValidateIssuerSigningKey = true;
            options.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });
    

    I've seen other answers change other settings, such as ClockSkew; the defaults are set such that it should work for distributed environments whose clocks aren't exactly in sync. These are the only settings you need to change.

  3. Set up Authentication. You should have this line before any middleware that requires your User info, such as app.UseMvc().

    app.UseAuthentication();
    

    Note that this will not cause your token to be emitted with the SignInManager or anything else. You will need to provide your own mechanism for outputting your JWT - see below.

  4. You may want to specify an AuthorizationPolicy. This will allow you to specify controllers and actions that only allow Bearer tokens as authentication using [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
    
  5. Here comes the tricky part: building the token.

    class JwtSignInHandler
    {
        public const string TokenAudience = "Myself";
        public const string TokenIssuer = "MyProject";
        private readonly SymmetricSecurityKey key;
    
        public JwtSignInHandler(SymmetricSecurityKey symmetricKey)
        {
            this.key = symmetricKey;
        }
    
        public string BuildJwt(ClaimsPrincipal principal)
        {
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
            var token = new JwtSecurityToken(
                issuer: TokenIssuer,
                audience: TokenAudience,
                claims: principal.Claims,
                expires: DateTime.Now.AddMinutes(20),
                signingCredentials: creds
            );
    
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
    

    Then, in your controller where you want your token, something like the following:

    [HttpPost]
    public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory)
    {
        var principal = new System.Security.Claims.ClaimsPrincipal(new[]
        {
            new System.Security.Claims.ClaimsIdentity(new[]
            {
                new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User")
            })
        });
        return tokenFactory.BuildJwt(principal);
    }
    

    Here, I'm assuming you already have a principal. If you are using Identity, you can use IUserClaimsPrincipalFactory<> to transform your User into a ClaimsPrincipal.

  6. To test it: Get a token, put it into the form at jwt.io. The instructions I provided above also allow you to use the secret from your config to validate the signature!

  7. If you were rendering this in a partial view on your HTML page in combination with the bearer-only authentication in .Net 4.5, you can now use a ViewComponent to do the same. It's mostly the same as the Controller Action code above.


非常感谢您的回答!我只是想知道 - 您认为使用HMAC-SHA256签名我的字符串并发布此类令牌怎么样?我只是想知道这是否是一个足够安全的解决方案 :) - Patryk Golebiowski
1
我绝不是一个安全专家 - 评论框无法为我留下详尽的解释。这真的取决于您的用例,但我相信旧版ASP.Net使用了机器密钥,通常是SHA256,当人们进行自定义时。 - Matt DeKrey
2
@MattDeKrey 注意,RSACryptoServiceProvider.ToXmlStringRSACryptoServiceProvider.FromXmlString没有被移植到CoreCLR。这意味着当使用这些方法时,您将无法针对dnxcore50进行目标设置。 - Kévin Chalet
1
@Randolph,如果资源服务器(也就是您的“API”)和授权服务器(创建令牌的组件)不属于同一应用程序,则不建议使用对称算法来签署访问令牌。建议您真正使用RSA-SHA512,正如Matt所建议的那样。 - Kévin Chalet
1
@Randolph 最后一条建议:如果您计划支持外部客户端(即您不拥有的客户端),则真的应考虑采用标准协议,例如OAuth2或OpenID Connect,而不是创建自己的终端节点。如果需要更多信息,请参阅我的答案。 - Kévin Chalet
显示剩余3条评论

4
为了实现您所描述的功能,您需要同时使用OAuth2/OpenID Connect授权服务器和一个中间件来验证API的访问令牌。 Katana曾经提供过一个OAuthAuthorizationServerMiddleware,但在ASP.NET Core中已不再存在。
我建议您查看AspNet.Security.OpenIdConnect.Server,这是一个实验性的OAuth2授权服务器中间件分支,该教程中使用了它:有一个OWIN/Katana 3版本,以及一个支持net451(.NET桌面)和netstandard1.4(与.NET Core兼容)的ASP.NET Core版本。

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server

不要错过MVC Core示例,它展示了如何使用AspNet.Security.OpenIdConnect.Server配置OpenID Connect授权服务器以及如何验证由服务器中间件签发的加密访问令牌。https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Mvc/Mvc.Server/Startup.cs 您还可以阅读此博客文章,该文章解释了如何实现资源所有者密码授权,这是基本身份验证的OAuth2等效方法。http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-resource-owner-password-credentials-grant/

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication();
    }

    public void Configure(IApplicationBuilder app)
    {
        // Add a new middleware validating the encrypted
        // access tokens issued by the OIDC server.
        app.UseOAuthValidation();

        // Add a new middleware issuing tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.TokenEndpointPath = "/connect/token";

            // Override OnValidateTokenRequest to skip client authentication.
            options.Provider.OnValidateTokenRequest = context =>
            {
                // Reject the token requests that don't use
                // grant_type=password or grant_type=refresh_token.
                if (!context.Request.IsPasswordGrantType() &&
                    !context.Request.IsRefreshTokenGrantType())
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
                        description: "Only grant_type=password and refresh_token " +
                                     "requests are accepted by this 
                    return Task.FromResult(0);
                }

                // Since there's only one application and since it's a public client
                // (i.e a client that cannot keep its credentials private),
                // call Skip() to inform the server the request should be
                // accepted without enforcing client authentication.
                context.Skip();

                return Task.FromResult(0);
            };

            // Override OnHandleTokenRequest to support
            // grant_type=password token requests.
            options.Provider.OnHandleTokenRequest = context =>
            {
                // Only handle grant_type=password token requests and let the
                // OpenID Connect server middleware handle the other grant types.
                if (context.Request.IsPasswordGrantType())
                {
                    // Do your credentials validation here.
                    // Note: you can call Reject() with a message
                    // to indicate that authentication failed.

                    var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
                    identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");

                    // By default, claims are not serialized
                    // in the access and identity tokens.
                    // Use the overload taking a "destinations"
                    // parameter to make sure your claims
                    // are correctly inserted in the appropriate tokens.
                    identity.AddClaim("urn:customclaim", "value",
                        OpenIdConnectConstants.Destinations.AccessToken,
                        OpenIdConnectConstants.Destinations.IdentityToken);

                    var ticket = new AuthenticationTicket(
                        new ClaimsPrincipal(identity),
                        new AuthenticationProperties(),
                        context.Options.AuthenticationScheme);

                    // Call SetScopes with the list of scopes you want to grant
                    // (specify offline_access to issue a refresh token).
                    ticket.SetScopes("profile", "offline_access");

                    context.Validate(ticket);
                }

                return Task.FromResult(0);
            };
        });
    }
}

project.json

{
  "dependencies": {
    "AspNet.Security.OAuth.Validation": "1.0.0",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0"
  }
}

祝你好运!


已更新以针对ASP.NET Core RTM和ASOS beta6。 - Kévin Chalet

3
您可以使用OpenIddict来提供token(登录),然后在API/Controller访问时使用UseJwtBearerAuthentication进行验证。
这基本上是您在Startup.cs中需要的所有配置: ConfigureServices:
services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    // this line is added for OpenIddict to plug in
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

配置

app.UseOpenIddictCore(builder =>
{
    // here you tell openiddict you're wanting to use jwt tokens
    builder.Options.UseJwtTokens();
    // NOTE: for dev consumption only! for live, this is not encouraged!
    builder.Options.AllowInsecureHttp = true;
    builder.Options.ApplicationCanDisplayErrors = true;
});

// use jwt bearer authentication to validate the tokens
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    // must match the resource on your token request
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

有一两个其他的小问题,比如您的DbContext需要派生自OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>

您可以在我的博客文章中查看完整的解释(包括运行的github存储库): http://capesean.co.za/blog/asp-net-5-jwt-tokens/


2
您可以查看OpenId连接示例,这些示例说明如何处理不同的身份验证机制,包括JWT令牌:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

如果您查看 Cordova 后端项目,API 的配置如下:
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), 
      branch => {
                branch.UseJwtBearerAuthentication(options => {
                    options.AutomaticAuthenticate = true;
                    options.AutomaticChallenge = true;
                    options.RequireHttpsMetadata = false;
                    options.Audience = "localhost:54540";
                    options.Authority = "localhost:54540";
                });
    });

在/Providers/AuthorizationProvider.cs文件和该项目的RessourceController中的逻辑也值得一看。此外,我使用Aurelia前端框架和ASP.NET Core实现了一个基于令牌的身份验证单页面应用程序。还有一个Signal R持久连接。不过,我没有进行任何数据库实现。代码可以在这里看到:https://github.com/alexandre-spieser/AureliaAspNetCoreAuth 希望这能帮到你,
祝好,
Alex

直到我发现受众没有方案(因此是localhost:54540而不是http://localhost:54540),它才对我起作用。当我改变了那个时,它就像魅力一样工作! - kloarubeek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接