.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, };