ASP.NET核心中基于令牌的身份验证(刷新)

Pat*_*ski 68 c# authentication authorization asp.net-web-api asp.net-core

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

我的场景: 客户端请求令牌.我的服务器应该授权用户并返回access_token,客户端将在以下请求中使用该access_token.

这里有两篇关于正确实现我需要的文章:

问题是 - 对于我来说,如何在ASP.NET Core中执行相同的操作并不明显.

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

谢谢!

Mar*_*hes 71

根据Matt Dekrey的精彩回答,我创建了一个基于令牌的身份验证的完整工作示例,针对ASP.NET Core(1.0.1).你可以找到完整的代码在这个仓库在GitHub上(替代分支1.0.0-RC1,beta8,β7的),但在短暂的,重要的步骤是:

为您的应用程序生成密钥

在我的例子,我生成应用程序启动随机密钥每一次,你需要生成一个和它存储在某处,并将其提供给您的应用程序.看到这个文件,我如何生成一个随机密钥,以及如何你可能会从一个以.json文件中导入.作为由@kspearrin的意见建议,对数据保护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());
});
Run Code Online (Sandbox Code Playgroud)

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

Startup.cs - 配置

在这里,我们需要配置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)
    }
});
Run Code Online (Sandbox Code Playgroud)

TokenController

在令牌控制器中,您需要有一个方法来使用Startup.cs中加载的密钥生成签名密钥.我们在Startup中注册了一个TokenAuthOptions实例,所以我们需要在T​​okenController的构造函数中注入它:

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

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...
Run Code Online (Sandbox Code Playgroud)

然后,您需要在处理程序中为登录端点生成令牌,在我的示例中,我使用用户名和密码并使用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);
}
Run Code Online (Sandbox Code Playgroud)

这应该是它.只需添加[Authorize("Bearer")]到要保护的任何方法或类,如果在没有令牌存在的情况下尝试访问它,则应该收到错误.如果要返回401而不是500错误,则需要在我的示例中注册自定义异常处理程序.

  • 这个和**只有这个答案**适用于ASP.NET 5 RC,在鞭打互联网和同事寻求解决方案之后!非常感谢你,@ MarkHughes,你应该**真的**用你的优秀例子写下你自己的问答答案. (4认同)
  • 好的,我找到了:标题名称应该是:"授权"和值:"Bearer [token]" (3认同)
  • 是否有可能只添加[授权("承载")]而只添加[授权]? (2认同)
  • 我相信这会工作@zoranpro - 只要你在startup.cs中注册了一个身份验证中间件.如果您有多个注册,那么[授权]将允许通过任何这些方法进行身份验证的人访问 - 根据您的使用情况,这可能会很好. (2认同)
  • @MarkHughes请更新RC2,因为您的UseJwtBearerAuthentication语法不再有效 (2认同)

Mat*_*rey 24

这真的是我的另一个答案的重复,我倾向于保持更新,因为它得到更多的关注.那里的评论也可能对你有用!

针对.Net Core 2进行了更新:

此答案的先前版本使用RSA; 如果生成令牌的相同代码也在验证令牌,则实际上没有必要.但是,如果您正在分配责任,您可能仍希望使用的实例执行此操作Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. 创建一些我们稍后将使用的常量; 这就是我做的:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    
    Run Code Online (Sandbox Code Playgroud)
  2. 将其添加到您的Startup.cs中ConfigureServices.我们稍后将使用依赖注入来访问这些设置.我假设你authenticationConfiguration是一个ConfigurationSection或一个Configuration对象,你可以有一个不同的配置进行调试和生产.确保您安全地存放钥匙!它可以是任何字符串.

    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;
        });
    
    Run Code Online (Sandbox Code Playgroud)

    我已经看到其他答案改变了其他设置,例如ClockSkew; 设置默认值,使其适用于时钟不完全同步的分布式环境.这些是您需要更改的唯一设置.

  3. 设置身份验证.在任何需要您的User信息的中间件之前,您应该拥有此行,例如app.UseMvc().

    app.UseAuthentication();
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,这不会导致令牌与SignInManager其他任何内容一起发出.您需要提供自己的输出JWT的机制 - 见下文.

  4. 您可能想要指定一个AuthorizationPolicy.这将允许您指定仅允许使用承载令牌作为身份验证的控制器和操作[Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
    
    Run Code Online (Sandbox Code Playgroud)
  5. 这里有一个棘手的部分:构建令牌.

    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);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    然后,在您需要令牌的控制器中,如下所示:

    [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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    在这里,我假设你已经有了校长.如果您使用的是身份,则可以使用IUserClaimsPrincipalFactory<>将您User转换为ClaimsPrincipal.

  6. 测试它:获取一个令牌,将其放入jwt.io的表单中.我上面提供的说明还允许您使用配置中的秘密来验证签名!

  7. 如果您在HTML页面上的部分视图中结合使用.Net 4.5中的仅承载身份验证,您现在可以使用a ViewComponent来执行相同操作.它与上面的Controller Action代码大致相同.

  • @MattDeKrey注意到`RSACryptoServiceProvider.ToXmlString`和`RSACryptoServiceProvider.FromXmlString`尚未移植到CoreCLR.这意味着在使用这些方法时,您将无法定位`dnxcore50`. (2认同)