首页 > 解决方案 > 根据用户配置 JwtBearerOptions IssuerSigningKey

问题描述

我开始使用 Jwt 在 NET Core 中进行 API 身份验证,并且像往常一样,我阅读了一些示例和教程,我注意到的一件事是,它们中的大多数都SymmetricSecutityKey基于存储在某处的已知字符串生成(无论是文件还是硬编码)或随机输出。我设法使身份验证正常工作,但现在我坚持以下几点:如何设置StartUp.cs配置以便验证IssueSigningKey参数检查多个密钥?贝娄,一段工作代码:

认证控制器

var authClaims = new[] {
    new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.Ticks.ToString())
    };

var ssKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("7S79jvOkEdwoRqHx"));
var securityToken = new JwtSecurityToken(
    issuer: _apiSettings.BearerValidIssuer,
    audience: _apiSettings.BearerValidAudience,
    expires: DateTime.Now.AddHours(6),
    claims: authClaims,
    signingCredentials: new SigningCredentials(ssKey, SecurityAlgorithms.HmacSha256Signature)
    );

    return Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(securityToken),
        expiration = securityToken.ValidTo,
    });

而当前的StartUp.cs配置,关于承载令牌:

启动.cs

services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options => {
        options.SaveToken = true;
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
        {
            ValidateIssuer = true,
            ValidIssuer = _apiSettings.GetValue(typeof(string), "BearerValidIssuer").ToString(),
            ValidateAudience = true,
            ValidAudience = _apiSettings.GetValue(typeof(string), "BearerValidAudience").ToString(),
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("7S79jvOkEdwoRqHx")),
            };
         });

我知道,至少为了举例,将生成对称密钥的字符串存储在文件(例如 .json 配置文件)的某个位置是常见的做法,但我想生成它并存储它们与用户信息一起在数据库中。该密钥将在某个时候传递给用户,然后它将用于通过 REST 请求生成访问令牌。这是可以实现的吗?还添加如果这在安全性方面甚至是实用的,或者我可以“只是”在文件中记录字符串?

标签: c#jwtasp.net-core-5.0

解决方案


好吧,为了给有同样疑问的人留下参考,这就是我解决问题的方法。

首先,我必须修改我的身份验证控制器,以便JwtSecurityToken根据每个用户存储在数据库中的字符串创建签名凭据。该字符串是从请求的“授权”标头中检索的,并将与用户一起保存。这是你证明“你就是你”的部分。此授权令牌还有两个特定的声明,一个用于“WebApi”访问的角色和用户“uid”。

授权控制器

public async Task<IActionResult> GetAuthenticationToken([FromBody] JSONBody json)
    {
        try
        {
            User user = await _userManager.FindByEmailAsync(json.Email);
            if (user != null)
            {
                if (await _userManager.IsInRoleAsync(user, "WebApi"))
                {
                    if (HttpContext.Request.Headers.TryGetValue("Authorization", out StringValues headerValues))
                    {
                        string sskeyString = headerValues.First();
                        if (user.SymmetricSecurityKeyString == sskeyString)
                        {
                            var authClaims = new[] {
                            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                            new Claim(JwtRegisteredClaimNames.Email, user.Email),
                            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                            new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.Ticks.ToString()),
                            new Claim(ClaimTypes.Role, "WebApi"),
                            new Claim("uid", user.Id)
                        };

                            var ssKey = new SymmetricSecurityKey(Convert.FromBase64String(user.SymmetricSecurityKeyString));
                            var securityToken = new JwtSecurityToken(
                                issuer: _apiSettings.BearerValidIssuer,
                                audience: _apiSettings.BearerValidAudience,
                                expires: DateTime.Now.AddHours(6),
                                claims: authClaims,
                                signingCredentials: new SigningCredentials(ssKey, SecurityAlgorithms.HmacSha256Signature)
                                );

                            return Ok(new
                            {
                                token = new JwtSecurityTokenHandler().WriteToken(securityToken),
                                expiration = securityToken.ValidTo,
                            });
                        }
                        else
                            return Unauthorized(new { Message = "Token de authenticação Inválido." });
                    }
                    else
                        return Unauthorized(new { Message = "Header Authorization não encontrado." });
                }
                else
                    return Unauthorized(new { Message = "Este usuário não possui acesso à WebApi. Contate a DescontaNet para solicitar este perfil de acesso." });
            }
            else
                return NotFound(new { Message = $"Usuário não encontrado para este email: {json.Email}" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, ex.InnerException);
        }
    }

在阅读了有关该AddAuthentication().AddJwtBearer()方法的更多信息后,我了解到您可以设置多个身份验证方案,每个方案都有自己的选项。最重要的选项是SecurityTokenValidators属性,它是一个IList<ISecurityTokenValidators>. 这将验证身份验证期间发送到 API 的任何令牌。所以StartUp.cs现在看起来像这样:

启动.cs

services.AddAuthentication()
            .AddJwtBearer("apiToken", options => {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidIssuer = _apiSettings.GetValue(typeof(string), "BearerValidIssuer").ToString(),
                    ValidateAudience = true,
                    ValidAudience = _apiSettings.GetValue(typeof(string), "BearerValidAudience").ToString(),
                    ValidateIssuerSigningKey = true,
                };
                options.SecurityTokenValidators.Clear();
                options.SecurityTokenValidators.Add(new DynamicKeyJwtValidationHandler(Configuration.GetConnectionString("DNDrive")));
            });
// There is also a policy to require this specific token validation in certain methods
services.AddAuthorization(options => {
            options.AddPolicy("WebApiPolicy", policy => {
                policy.RequireRole("WebApi");
                policy.RequireAuthenticatedUser();
                policy.AddAuthenticationSchemes("apiToken");
            });

一个次要但重要的细节:调用services.SecurityTokenValidators.Clear()可确保删除所有默认方案,并且在身份验证期间仅检查您的方案。说到这,我不得不自己查找并制作一个验证器,如下所示:

DynamicKeyJwtTokenValidationHandler 类

public class DynamicKeyJwtValidationHandler : JwtSecurityTokenHandler, ISecurityTokenValidator
{
    private DNDriveContext db;
    public DynamicKeyJwtValidationHandler(string connectionStr)
    {
        var optionsBuilder = new DbContextOptionsBuilder<DNDriveContext>();
        optionsBuilder.UseSqlServer(connectionStr);
        db = new DNDriveContext(optionsBuilder.Options);
    }

    private SecurityKey GetSSKeyForId(string id)
    {
        var user = db.User.Where(u => u.Id == id).FirstOrDefault();
        if (user == null)
            throw new Exception("User Id not found");
        return new SymmetricSecurityKey(Convert.FromBase64String(user.SymmetricSecurityKeyString));
    }

    public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        // Read the token before starting validation
        JwtSecurityToken incomingToken = ReadJwtToken(token);
        // Extract external system ID from the token
        string externalSystemId = incomingToken.Claims.First(claim => claim.Type == "uid").Value;
        // Retrieve the Symmetric Security Key String from the database
        SecurityKey publicKeyForExternalSystem = GetSSKeyForId(externalSystemId);
        // Set up the Security Key for that user
        validationParameters.IssuerSigningKey = publicKeyForExternalSystem;
        // Framework default validation
        return base.ValidateToken(token, validationParameters, out validatedToken);
    }
}

该类继承自JwtSecurityTokenHandler进行验证并实现ISecurityTokenValidator接口。在我的例子中,每个用户都有一个单独的令牌来验证是很重要的,因此我访问数据库并SymmetricSecurityKey从存储在数据库中的字符串中检索一个。如何在数据库中搜索用户?此令牌具有Claim“uid”类型的自定义。使用SecurityKey它应该进行验证,我调用该base.ValidateToken()方法,传递传入的令牌并返回结果。

有了这个,以及在 中创建的策略StartUp.cs,我可以轻松地验证具有此标记的控制器下的任何方法的令牌[Authorize(Policy = "WebApiPolicy")]:可能有一些方法可以提高安全性,因为最终用户仍然必须保证他的身份验证密钥的安全,但至少我设法添加了额外的验证层,并且 JWT 令牌有一个到期日期。


推荐阅读