ASP.NET Core中的基于令牌的身份验证

173
我正在使用ASP.NET Core应用程序。我尝试实现基于令牌的身份验证,但无法弄清楚如何为我的情况使用新的Security System。我查看了示例,但它们并没有帮助我太多,它们使用的是cookie身份验证或外部身份验证(GitHub、Microsoft、Twitter)。
我的场景是:angularjs应用程序应该请求/token url,传递用户名和密码。WebApi应该授权用户并返回access_token,这将在后续请求中由angularjs应用程序使用。
我找到了一篇关于在ASP.NET的当前版本中实现我所需内容的优秀文章 - Token Based Authentication using ASP.NET Web API 2, Owin, and Identity。但我不知道如何在ASP.NET Core中做同样的事情。
我的问题是:如何配置ASP.NET Core WebApi应用程序以使用基于令牌的身份验证?

https://dev59.com/pF4b5IYBdhLWcg3wfhuR#29144031 - Andrei Neculai
4个回答

147

.Net Core 3.1更新:

ASP .NET Core团队架构师David Fowler创建了一组非常简单的任务应用程序,其中包括一个演示JWT的简单应用程序。我将很快将他的更新和简单风格融入到这篇文章中。

.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.


1
您需要实际注入IOptions<OAuthBearerAuthenticationOptions>才能使用Options; 直接使用Options对象是不支持的,因为Options模型框架支持命名配置。 - Matt DeKrey
2
更新为我正在使用的内容,不过现在这个答案需要重新编写。谢谢提醒! - Matt DeKrey
6
在Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6及可能之前的beta版本中, #5已被更改为以下内容:auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(OAuthBearerAuthenticationDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());该代码段为授权策略的添加,使用OAuthBearer身份验证方案对用户进行身份验证并要求用户进行身份验证。 - dynamiclynk
5
我会尽力进行翻译,以下是您需要翻译的内容:@MattDeKrey 我以这个答案为起点,用它来示范简单的基于令牌的身份验证,并将其更新为适用于 beta 7 版本——请参见 https://github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - 还包括了这些评论中提到的几个要点。 - Mark Hughes
2
已更新至RC1版本 - GitHub上的分支中提供了Beta7和Beta8的旧版本。Beta7Beta8 - Mark Hughes
显示剩余28条评论

85

参考Matt Dekrey的精彩回答,我创建了一个完全可用的基于令牌的身份验证的示例,可以在ASP.NET Core (1.0.1)上工作。您可以在GitHub上的此存储库中找到完整的代码(包括1.0.0-rc1, beta8, beta7等替代分支),但简要来说,重要步骤如下:

为应用程序生成密钥

在我的示例中,每次启动应用程序时都会生成一个随机密钥,您需要生成并将其存储在某个位置,并提供给您的应用程序。可以查看此文件以了解如何生成随机密钥以及如何从.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

在token控制器中,需要有一个使用在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")]添加到您想要保护的任何方法或类中,如果尝试在没有令牌的情况下访问它,则应该会收到错误提示。如果要返回401而不是500错误,则需要注册自定义异常处理程序,就像我在此示例中做的那样


1
这是一个非常出色的例子,包含了我需要让@MattDeKrey的例子工作的所有缺失部分,非常感谢!请注意,任何仍然针对beta7而不是beta8的人仍然可以在github历史记录中找到该示例。 - nickspoon
我怀疑@kspearrin有一种方法可以做到这一点 - 如果你知道怎么做,请告诉我,我会添加进去!我明天会看一下,看看能否解决这个问题。 - Mark Hughes
2
谢谢您的回复,但我不太明白为什么在ASP.Net 4 Web API中开箱即用的东西现在在ASP.Net 5中需要进行相当多的配置。这似乎是一步倒退。 - JMK
2
我认为他们正在极力推广ASP.NET 5的“社交认证”,这在某种程度上是有道理的,但有些应用程序可能并不适合使用这种功能,因此我不确定自己是否同意他们的方向。@JMK - Mark Hughes
1
更新至 dotnet core 1.0.1,供有兴趣的人参考。 - Mark Hughes
显示剩余5条评论

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

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

如果您查看Cordova后端项目,API的配置如下所示:
           // Create a new branch where the registered middleware will be executed only for non API calls.
        app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch => {
            // Insert a new cookies middleware in the pipeline to store
            // the user identity returned by the external identity provider.
            branch.UseCookieAuthentication(new CookieAuthenticationOptions {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                AuthenticationScheme = "ServerCookie",
                CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie",
                ExpireTimeSpan = TimeSpan.FromMinutes(5),
                LoginPath = new PathString("/signin"),
                LogoutPath = new PathString("/signout")
            });

            branch.UseGoogleAuthentication(new GoogleOptions {
                ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
                ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
            });

            branch.UseTwitterAuthentication(new TwitterOptions {
                ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
                ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
            });
        });

在/Providers/AuthorizationProvider.cs文件中的逻辑和该项目的RessourceController也值得一看。 ;)
或者,您也可以使用以下代码验证令牌(还有一个片段可使其与signalR配合使用):
        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;

            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

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

要发行令牌,您可以像这样使用OpenID Connect服务器包:

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the authorization, logout, token and userinfo endpoints.
            //options.AuthorizationEndpointPath = "/connect/authorize";
            //options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            //options.UserinfoEndpointPath = "/connect/userinfo";

            // Note: if you don't explicitly register a signing key, one is automatically generated and
            // persisted on the disk. If the key cannot be persisted, an exception is thrown.
            // 
            // On production, using a X.509 certificate stored in the machine store is recommended.
            // You can generate a self-signed certificate using Pluralsight's self-cert utility:
            // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip
            // 
            // options.SigningCredentials.AddCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
            // 
            // Alternatively, you can also store the certificate as an embedded .pfx resource
            // directly in this assembly or in a file published alongside this project:
            // 
            // options.SigningCredentials.AddCertificate(
            //     assembly: typeof(Startup).GetTypeInfo().Assembly,
            //     resource: "Nancy.Server.Certificate.pfx",
            //     password: "Owin.Security.OpenIdConnect.Server");

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            options.ApplicationCanDisplayErrors = true // in dev only ...;
            options.AllowInsecureHttp = true // in dev only...;
        });

我使用Aurelia前端框架和ASP.NET Core实现了一个单页应用程序,采用基于令牌的身份验证实现。还有一个SignalR持久连接。但是,我没有进行任何数据库实现。 代码在这里: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

1
请查看OpenIddict - 这是一个新项目(在撰写本文时),它使得在ASP.NET 5中配置创建JWT令牌和刷新令牌变得容易。令牌的验证由其他软件处理。
假设您使用Identity与Entity Framework,那么最后一行是您将添加到ConfigureServices方法中的内容:
services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

在“配置”中,您设置OpenIddict以提供JWT令牌:
app.UseOpenIddictCore(builder =>
{
    // 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;
});

你还可以在Configure中配置令牌验证:
// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

还有一两件小事,例如您的DbContext需要派生自OpenIddictContext。

您可以在此博客文章中查看完整的解释:http://capesean.co.za/blog/asp-net-5-jwt-tokens/

一个可运行的演示可在此处找到:https://github.com/capesean/openiddict-test


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