Asp.Net Core 集成JWT采用Rsa非对称密钥并实现自定义身份验证

发布时间 2023-07-28 17:43:12作者: 大西瓜3721

授权和鉴权分为了两个项目。

首先是授权:

建立Asp.net core 项目,并在Nuget包安装 System.IdentityModel.Tokens.Jwt
新建一个Web Api 用于登录,这里使用账户密码方便调试。另外BaseResult是我封装的一个统一返回数据类型。需要注意的是 audience 以及 JwtRegisteredClaimNames.Name 都不应该固定写死(如果有三方或多客户端登录等...),我这里只是方便调试。具体请到Jwt查看相关资料。
/// <summary>
/// 账户密码登录
/// </summary>
/// <param name="loginModel"></param>
/// <returns></returns>
[HttpPost]
public BaseResult Post(LoginModel loginModel)
{
//检查账户密码
Users user = usersManage.GetUserByAccountAndPwd(loginModel.AccountName, loginModel.Password);

if (user == null)
{
return new BaseResult { result = "error", code = E_ResultCode.error, message = "账号名或密码错误" };
}


var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Name, "ZxManagementPlatform"),
new Claim("ID",user.ID)
//此处可以自定义添加所需要的数据...
};

claims.AddRange(roles.Select(x => new Claim("Role", x.RoleCode)));

//=====================================================================
//对称
//var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConfig.JwtSecurityKey));

//=====================================================================
//非对称

var rsa = RSA.Create();
byte[] publicKey = Convert.FromBase64String("公钥Base64(掐头去尾,不带Begin...以及回车空格等格式的) ---这里其实不需要公钥,但是可以作为保存");
byte[] privateKey = Convert.FromBase64String("私钥Base64(掐头去尾,不带Begin...以及回车空格等格式的) ---我是用的是 Pkcs8格式的密钥");

rsa.ImportPkcs8PrivateKey(privateKey, out _);

var key = new RsaSecurityKey(rsa);
PrivateKeyStatus privateKeyStatus = key.PrivateKeyStatus;

//=====================================================================

var token = new JwtSecurityToken(
issuer: GlobalConfig.JwtIssuer,
audience: "zxservice",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddHours(2),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.RsaSha256)
//signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);

var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

return new BaseResult { result = "success", code = E_ResultCode.success, message = "成功", data = jwtToken };
}

然后是鉴权

建立Asp.net core 项目,并在Nuget包安装 Microsoft.AspNetCore.Authentication.JwtBearer
添加自定义身份校验代码
这个部分参考于 https://blog.csdn.net/weixin_33854644/article/details/85018389 ,并修改了一部分。
首先建立文件夹JwtPolicy 并添加文件和代码
//==================================================================================
//============================ 文件一

/// <summary>
/// 用户或角色或其他凭据实体
/// </summary>
public class Permission
{
/// <summary>
/// 用户或角色或其他凭据名称
/// </summary>
public virtual string Name { get; set; }
/// <summary>
/// 请求Url
/// </summary>
public virtual string Url { get; set; }
}

//==================================================================================
//============================ 文件二

/// <summary>
/// 权限授权Handler
/// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
/// <summary>
/// 验证方案提供对象
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; }

/// <summary>
/// 自定义策略参数
/// </summary>
public PermissionRequirement Requirement { get; set; }

/// <summary>
/// 没有权限,禁止访问
/// </summary>
private static readonly string ForbiddenResult = JsonConvert.SerializeObject(new BaseResult { code = E_ResultCode.Forbidden, result = "error", message = "没有权限,禁止访问" });

/// <summary>
/// 没有登录
/// </summary>
private static readonly string UnauthorizedResult = JsonConvert.SerializeObject(new BaseResult { code = E_ResultCode.Unauthorized, result = "error", message = "请先登录" });

/// <summary>
/// 构造
/// </summary>
/// <param name="schemes"></param>
public PermissionHandler(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}

/// <summary>
/// 权限校验
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{

//赋值用户权限
Requirement = requirement;
//从AuthorizationHandlerContext转成HttpContext,以便取出表求信息
var httpContext = (context.Resource as Microsoft.AspNetCore.Http.DefaultHttpContext).HttpContext;


//====================================================================================

//请求Url
var questUrl = httpContext.Request.Path.Value.ToLower();
//请求的方式
var questMethod = httpContext.Request.Method.ToLower();

//判断请求是否停止
var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync())
{
context.Fail();
return;
}
}
//判断请求是否拥有凭据,即有没有登录
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);
//result?.Principal不为空 并且 Succeeded == true 即登录成功
if (result?.Succeeded == true && result?.Principal != null)
{
httpContext.User = result.Principal;
//权限中是否存在请求的url
if (Requirement.Permissions.Where(x => questUrl.ToLower().StartsWith(x.Url.ToLower())).Any())
{
List<string> LtRoles = httpContext.User.Claims.Where(s => s.Type == requirement.ClaimType).Select(x => x.Value).ToList();

//验证权限
if (!Requirement.Permissions.Where(w => LtRoles.Contains(w.Name) && questUrl.ToLower().StartsWith(w.Url.ToLower()) && w.Method.ToLower() == questMethod).Any())
{
//无权限拒绝访问
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(ForbiddenResult);
context.Fail();
}

}
context.Succeed(requirement);
return;
}
else
{
//没有登录
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(UnauthorizedResult);
context.Fail();
}
}

//判断没有登录时,是否访问登录的url,并且是Post请求,并助是form表单提交类型,否则为失败
if (!questUrl.Equals(Requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))
{
//没有登录
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(UnauthorizedResult);
context.Fail();
}

context.Succeed(requirement);
}
}
//==================================================================================
//============================ 文件三

/// <summary>
/// 必要参数类
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// 用户权限集合
/// </summary>
public List<Permission> Permissions { get; private set; }
/// <summary>
/// 无权限action
/// </summary>
public string DeniedAction { get; set; }
/// <summary>
/// 认证授权类型
/// </summary>
public string ClaimType { internal get; set; }
/// <summary>
/// 请求路径
/// </summary>
public string LoginPath { get; set; } = "/Api/Login";
/// <summary>
/// 发行人
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// 订阅人
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5000);
/// <summary>
/// 签名验证
/// </summary>
public SigningCredentials SigningCredentials { get; set; }

/// 构造
/// </summary>
/// <param name="deniedAction">拒约请求的url</param>
/// <param name="permissions">权限集合</param>
/// <param name="claimType">声明类型</param>
/// <param name="issuer">发行人</param>
/// <param name="audience">订阅人</param>
/// <param name="signingCredentials">签名验证实体</param>
public PermissionRequirement(string deniedAction, List<Permission> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials)
{
ClaimType = claimType;
DeniedAction = deniedAction;
Permissions = permissions;
Issuer = issuer;
Audience = audience;
SigningCredentials = signingCredentials;
}
}

在Startup中的 ConfigureServices 方法中注册服务。其中 rsa.ImportPkcs8PublicKey 这是一个扩展方法,来源于 RSAExtensions 包,大家看一下这位大哥的Github https://github.com/stulzq/RSAExtensions 。这个包提供了导入 pkcs8 格式公钥的方法,以及其他好用的方法。
public void ConfigureServices(IServiceCollection services)
{

#region 其他服务注册
...
#endregion



//添加JWT验证 对称密钥 --- 弃用
//services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// .AddJwtBearer(option =>
// {
// option.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
// {
// ValidateIssuer = true,
// ValidateAudience = true,
// ValidateLifetime = true,
// ValidateIssuerSigningKey = true,
// ValidAudience = "zxservice",
// ValidIssuer = GlobalConfig.JwtIssuer,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConfig.JwtSecurityKey)),


// };
// });



//================================================================================================================
// 自定义权限校


//对称密钥
//var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConfig.JwtSecurityKey)), SecurityAlgorithms.HmacSha256);

//非对称密钥
var rsa = RSA.Create();
byte[] publicKey = Convert.FromBase64String("公钥Base64(掐头去尾,不带Begin...以及回车空格等格式的)");

//rsa.ImportPkcs8PublicKey 这是一个扩展方法,来源于 RSAExtensions 包,大家可以关注一下这位大哥的Github https://github.com/stulzq/RSAExtensions 。这个包提供了导入 pkcs8 格式公钥的方法。
rsa.ImportPkcs8PublicKey(publicKey);

var sKey = new RsaSecurityKey(rsa);

//非对称密钥
var signingCredentials = new SigningCredentials(sKey, SecurityAlgorithms.RsaPKCS1);

var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingCredentials.Key,
ValidateIssuer = true,
ValidIssuer = GlobalConfig.JwtIssuer,
ValidateAudience = true,
ValidAudience = GlobalConfig.JwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};


services.AddAuthorization(option =>
{
//这个集合模拟用户权限表,可从数据库中查询出来
var permission = new List<Permission> {
new Permission { Url="/api/v1/Test", Name="supadmin", Method="get" },
new Permission { Url="/api/v1/MyBasicInfo", Name="supadmin", Method="get" },
};

var permissionRequirement = new PermissionRequirement("", permission, "Role", GlobalConfig.JwtIssuer, GlobalConfig.JwtAudience, signingCredentials);

//添加自定义验证规则
option.AddPolicy("ZxPermission", policy => policy.Requirements.Add(permissionRequirement));

}).AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = GlobalConfig.JwtAudience,
ValidIssuer = GlobalConfig.JwtIssuer,
IssuerSigningKey = signingCredentials.Key,
};
});

services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

//================================================================================================================

}

 

测试

首先在 swagger 中调用api获取 token


然后用 PostMan 模拟请求并带上Token 注意 Type 要选择 Bearer Token


其实也可以访问 https://jwt.io/ 校验Token 。不过需要注意的是,在https://jwt.io/ 上输入公钥时 必须带上 -----BEGIN PUBLIC KEY----- 以及 -----END PUBLIC KEY----- 不然无法验签。


***注意*** 以上文章仅作为个人学习记录,文中的方案有很多还未详细处理。因为怕日后忘了,所以先记录下来。如有错误还请指正 谢谢!