.net core web api授权、鉴权、API保护

前言

  在开放的api接口中,我们通过http Post或者Get请求服务器的时候,会面临着许多的安全性问题。为了保证数据在通信时的安全性,我们可以采用TOKEN+参数签名的方式来进行相关验证。

  •   Token(本文使用jwt)来保证访问接口的用户身份合法,也是本篇介绍的重点。
  •   用Sign参数签名的方式来防止被篡改和请求的唯一性。

  本文所有的实现代码可以参考:https://gitee.com/xiaoqingyao/web-app-identity.git

用户管理

授权和鉴权的前提是要有一个用户管理模块,.net 提供一个现有的Identity组件,帮我们完成了大部分功能,包含:用户注册、用户登录验证等。这个是身份验证的基础。

详细使用可以参考:asp.net core使用identity+jwt保护你的webapi(一)——identity基础配置 - xhznl - 博客园

JWT

JWT服务配置及获取JwtToken

  用户登录后,就可以为用户颁发JwtToken,再请求其他业务接口的时候把该Token带过来(一般放到Header中)完成身份验证。

  详细使用可以参考:asp.net core使用identity+jwt保护你的webapi(二)——获取jwt token - xhznl - 博客园

弥补Jwt的先天缺钱——RefreshToken

  Jwt一旦颁发,基本不可控,在过期时间内一直有效。但是如果设置有效时间过短,又会导致用户频繁登录。refresh token就可以很好的弥补jwt的缺陷。在refresh token机制下,我们可以把token的有效期设置的短一些,比如30分钟,而refresh token的有效期可以很长;因为refresh token会持久化到数据库中,它是完全可控的。比如用户修改密码后我们同时失效refresh token实现用户重新登录。

  详细使用可以参考: asp.net core使用identity+jwt保护你的webapi(三)——refresh token - xhznl - 博客园

获取Jwt中的信息

  jwt中携带很多用户信息,我们也业务逻辑中也需要拿到该信息。

  我们以用户修改密码后失效refresh token为例演示如何解密并获取jwt中的信息。详细代码在前言中给到的gitee代码中。

  新增一个中间件

public class TokenMiddleware
{
    private readonly RequestDelegate _next;

    public TokenMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 从请求头中获取 token
        if (context.Request.Headers.TryGetValue("Authorization", out var token))
        {
            // 将 token 存入 HttpContext.Items
            context.Items["Token"] = token.ToString().Replace("Bearer ", ""); // 去除 "Bearer " 前缀,如果有的话
        }
        // 调用管道中的下一个中间件
        await _next(context);
    }
}

  封装一个HttpContextWrapper用于获取JwtToken

public interface IHttpContextWrapper
{
    string? GetToken();
}
public class HttpContextWrapper : IHttpContextWrapper
{
    private readonly Microsoft.AspNetCore.Http.HttpContext? _httpContent;

    public HttpContextWrapper(IHttpContextAccessor httpContextAccessor)
    {
        _httpContent = httpContextAccessor.HttpContext;
    }
    public string? GetToken()
    {
        if (_httpContent != null && _httpContent.Request.Headers.TryGetValue("Authorization", out var token))
        {
            return token.ToString().Replace("Bearer ", "");

        }
        return null;
    }
}

builder.Services.AddScoped<IHttpContextWrapper, HttpContextWrapper>(); // 注册自定义 HttpContextWrapper

  在controller中使用

/// <summary>
/// 修改密码
/// </summary>
/// <param name="modifyPasswordRequest"></param>
/// <returns></returns>
[HttpPost("ModifyPassword")]
public async Task<IActionResult> ModifyPassword(ModifyPasswordRequest modifyPasswordRequest)
{
    var token = _httpContextWrapper.GetToken();
    var result = await _userService.ModifyPassword(modifyPasswordRequest, token);
    if (!result.Success)
    {
        return BadRequest(new FailedResponse()
        {
            Errors = result.Errors
        });
    }
    return Ok();
}

业务代码中校验并获取token信息的关键代码

private ClaimsPrincipal? GetClaimsPrincipalByRsaToken(string token)
{


    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateIssuerSigningKey = true,
        //IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.SecurityKey)),//使用对称加密
        IssuerSigningKey = _securityKey,//使用公钥解签
        ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Aes128CbcHmacSha256 },//使用公钥解签
        TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abc111111111111111111111111111111cba")),//  token解密密钥 jwe解密
        ClockSkew = TimeSpan.Zero,
        ValidateLifetime = false
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    // 验证 JWT
    try
    {
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken);
        return principal;
    }
    catch (SecurityTokenExpiredException)
    {
        Console.WriteLine("JWT has expired.");
    }
    catch (SecurityTokenInvalidSignatureException)
    {
        Console.WriteLine("JWT signature is invalid.");
    }
    catch (Exception ex)
    {
        Console.WriteLine("JWT validation failed: " + ex.Message);
    }
    return null;
}

根据拿到的token信息找到对应的RefreshToken进行无效掉,这样token过期后就无法再通过refresh token进行token刷新了。

var storedRefreshToken =
    await _appDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token == refreshToken);
if (storedRefreshToken == null)
{
    // 无效的refresh_token...
    return new TokenResult()
    {
        Errors = new[] { "3: Invalid request!" },
    };
}
storedRefreshToken.Used = true;
await _appDbContext.SaveChangesAsync();

修改密码后,token还是没有过期的,这时候如何控制用户立马登录呢?

有很多技术手段比如修改jwt令牌,增加redis缓存校验等。我认为最好的办法就是前端配合把本地存储的jwttoken删除即可。

jwt升级:使用非对称加密进行Jwt签名和验签

公司内的多个业务项目都会使用该token,因此,为了让每个项目都可以进行身份认证,就需要将密钥分发给所有项目,这就产生了较大的风险。因此,使用非对称加密来计算签名,是一个更加合理地选择:我们使用私钥进行签名,然后只需要将公钥暴露出去用于验签,即可验证token是有效的(没有被篡改)。详细代码参考前言给的gitee。

新增RsaKeyManager用于导出公钥私钥

public class RsaKeyManager
{
    public void GenerateAndSaveRsaKeys(IWebHostEnvironment env)
    {
        var dir = Path.Combine(env.ContentRootPath, "Rsa");
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        if (File.Exists(Path.Combine(dir, "key.private.json"))
            && File.Exists(Path.Combine(dir, "key.public.json")))
        {
            return;
        }

        using (RSA rsa = RSA.Create(2048)) // 创建2048位的RSA密钥
        {
            // 导出公钥
            RSAParameters publicKeyParameters = rsa.ExportParameters(false);// 导出私钥
            RSAParameters privateKeyParameters = rsa.ExportParameters(true);
        }

        RSAParameters privateKey, publicKey;

        using (var rsa = RSA.Create(2048))
        {
            // 导出私钥
            privateKey = rsa.ExportParameters(true);
            // 导出公钥
            publicKey = rsa.ExportParameters(false);
        }
        File.WriteAllText(Path.Combine(dir, "key.public.json"), JsonConvert.SerializeObject(publicKey));
        File.WriteAllText(Path.Combine(dir, "key.private.json"), JsonConvert.SerializeObject(privateKey));
    }
}

在program中修改

var keyManager = new RsaKeyManager();
keyManager.GenerateAndSaveRsaKeys(builder.Environment);
var rsaSecurityPrivateKeyString = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "Rsa", "key.private.json"));
var rsaSecurityPublicKeyString = File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, "Rsa", "key.public.json"));
RsaSecurityKey rsaSecurityPrivateKey = new(Newtonsoft.Json.JsonConvert.DeserializeObject<RSAParameters>(rsaSecurityPrivateKeyString));
RsaSecurityKey rsaSecurityPublicKey = new(Newtonsoft.Json.JsonConvert.DeserializeObject<RSAParameters>(rsaSecurityPublicKeyString));

//使用私钥加签
builder.Services.AddSingleton(sp => new SigningCredentials(rsaSecurityPrivateKey, SecurityAlgorithms.RsaSha256));

builder.Services.AddSingleton(rsaSecurityPublicKey);
var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateIssuerSigningKey = true,
        //IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.SecurityKey)),//使用对称加密
        IssuerSigningKey = rsaSecurityPublicKey,//使用公钥解签
        ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },//使用公钥解签

        ClockSkew = TimeSpan.Zero,
    };

builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => { options.TokenValidationParameters = tokenValidationParameters; });

在业务代码中解密jwttoken

/// <summary>
/// 获取非对称加密token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private ClaimsPrincipal? GetClaimsPrincipalByRsaToken(string token)
{
    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateIssuerSigningKey = true,
        //IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.SecurityKey)),//使用对称加密
        IssuerSigningKey = _securityKey,//使用公钥解签
        ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },//使用公钥解签

        ClockSkew = TimeSpan.Zero,
        ValidateLifetime = false
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    // 验证 JWT
 var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken);
        return principal;

}

jwt升级2:使用JWE进行JWT加密

加密的时候添加

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(new[]
    {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString())
    }),
    IssuedAt = DateTime.UtcNow,
    NotBefore = DateTime.UtcNow,
    Expires = DateTime.UtcNow.Add(_jwtSettings.ExpiresIn),
    SigningCredentials = _signingCredentials//非对称加密
    ,
    EncryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abc111111111111111111111111111111cba")), JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes128CbcHmacSha256)
};

解密的时候添加

var tokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = false,
    ValidateAudience = false,
    ValidateIssuerSigningKey = true,
    //IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.SecurityKey)),//使用对称加密
    IssuerSigningKey = _securityKey,//使用公钥解签
    ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Aes128CbcHmacSha256 },//使用公钥解签
    TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abc111111111111111111111111111111cba")),//  token解密密钥 jwe解密
    ClockSkew = TimeSpan.Zero,
    ValidateLifetime = false
};

在program中同步修改解密配置

var tokenValidationParameters = new TokenValidationParameters
{
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateIssuerSigningKey = true,
                //IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.SecurityKey)),//使用对称加密
                IssuerSigningKey = rsaSecurityPublicKey,//使用公钥解签
                ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Aes128CbcHmacSha256 },//使用公钥解签
                                                                                                                 
                TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abc111111111111111111111111111111cba")),// token解密密钥 jwe解密
                ClockSkew = TimeSpan.Zero,
};

参考:【ASP.NET Core 认证】JwtBearer认证 - .Neterr - 博客园

posted @ 2024-10-21 11:29  chenxizhaolu  阅读(73)  评论(0编辑  收藏  举报