ASP.NET Core 使用 JWT 自定义角色/策略授权需要实现的接口
① 存储角色/用户所能访问的 API
例如
使用 List<ApiPermission>
存储角色的授权 API 列表。
可有可无。
可以把授权访问的 API 存放到 Token 中,Token 也可以只存放角色信息和用户身份信息。
/// <summary>
/// API
/// </summary>
public class ApiPermission
{
/// <summary>
/// API名称
/// </summary>
public virtual string Name { get; set; }
/// <summary>
/// API地址
/// </summary>
public virtual string Url { get; set; }
}
② 实现 IAuthorizationRequirement 接口
IAuthorizationRequirement
接口代表了用户的身份信息,作为认证校验、授权校验使用。
事实上,IAuthorizationRequirement
没有任何要实现的内容。
namespace Microsoft.AspNetCore.Authorization
{
//
// 摘要:
// Represents an authorization requirement.
public interface IAuthorizationRequirement
{
}
}
实现 IAuthorizationRequirement
,可以任意定义需要的属性,这些会作为自定义验证的便利手段。
要看如何使用,可以定义为全局标识,设置全局通用的数据。
我后面发现我这种写法不太好:
//IAuthorizationRequirement 是 Microsoft.AspNetCore.Authorization 接口
/// <summary>
/// 用户认证信息必要参数类
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// 用户所属角色
/// </summary>
public Role Roles { get; set; } = new Role();
public void SetRolesName(string roleName)
{
Roles.Name = roleName;
}
/// <summary>
/// 无权限时跳转到此API
/// </summary>
public string DeniedAction { get; set; }
/// <summary>
/// 认证授权类型
/// </summary>
public string ClaimType { internal get; set; }
/// <summary>
/// 未授权时跳转
/// </summary>
public string LoginPath { get; set; } = "/Account/Login";
/// <summary>
/// 发行人
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// 订阅人
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public TimeSpan Expiration { get; set; }
/// <summary>
/// 颁发时间
/// </summary>
public long IssuedTime { get; set; }
/// <summary>
/// 签名验证
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
/// <summary>
/// 构造
/// </summary>
/// <param name="deniedAction">无权限时跳转到此API</param>
/// <param name="userPermissions">用户权限集合</param>
/// <param name="deniedAction">拒约请求的url</param>
/// <param name="permissions">权限集合</param>
/// <param name="claimType">声明类型</param>
/// <param name="issuer">发行人</param>
/// <param name="audience">订阅人</param>
/// <param name="issusedTime">颁发时间</param>
/// <param name="signingCredentials">签名验证实体</param>
public PermissionRequirement(string deniedAction, Role Role, string claimType, string issuer, string audience, SigningCredentials signingCredentials,long issusedTime, TimeSpan expiration)
{
ClaimType = claimType;
DeniedAction = deniedAction;
Roles = Role;
Issuer = issuer;
Audience = audience;
Expiration = expiration;
IssuedTime = issusedTime;
SigningCredentials = signingCredentials;
}
}
③ 实现 TokenValidationParameters
Token 的信息配置
public static TokenValidationParameters GetTokenValidationParameters()
{
var tokenValida = new TokenValidationParameters
{
// 定义 Token 内容
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)),
ValidateIssuer = true,
ValidIssuer = AuthConfig.Issuer,
ValidateAudience = true,
ValidAudience = AuthConfig.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true
};
return tokenValida;
}
④ 生成 Token
用于将用户的身份信息(Claims)和角色授权信息(PermissionRequirement)存放到 Token 中。
/// <summary>
/// 获取基于JWT的Token
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
Status = true,
access_token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalMilliseconds,
token_type = "Bearer"
};
return response;
}
⑤ 实现服务注入和身份认证配置
从别的变量导入配置信息,可有可无
// 设置用于加密 Token 的密钥
// 配置角色权限
var roleRequirement = RolePermission.GetRoleRequirement(AccountHash.GetTokenSecurityKey());
// 定义如何生成用户的 Token
var tokenValidationParameters = RolePermission.GetTokenValidationParameters();
配置 ASP.NET Core 的身份认证服务
需要实现三个配置
- AddAuthorization 导入角色身份认证策略
- AddAuthentication 身份认证类型
- AddJwtBearer Jwt 认证配置
// 导入角色身份认证策略
services.AddAuthorization(options =>
{
options.AddPolicy("Permission",
policy => policy.Requirements.Add(roleRequirement));
// ↓ 身份认证类型
}).AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
// ↓ Jwt 认证配置
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
options.Events = new JwtBearerEvents()
{
// 在安全令牌通过验证和ClaimsIdentity通过验证之后调用
// 如果用户访问注销页面
OnTokenValidated = context =>
{
if (context.Request.Path.Value.ToString() == "/account/logout")
{
var token = ((context as TokenValidatedContext).SecurityToken as JwtSecurityToken).RawData;
}
return Task.CompletedTask;
}
};
});
注入自定义的授权服务 PermissionHandler
注入自定义认证模型类 roleRequirement
// 添加 httpcontext 拦截
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
services.AddSingleton(roleRequirement);
添加中间件
在微软官网看到例子是这样的。。。但是我测试发现,客户端携带了 Token 信息,请求通过验证上下文,还是失败,这样使用会返回403。
app.UseAuthentication();
app.UseAuthorization();
发现这样才OK:
app.UseAuthorization();
app.UseAuthentication();
参考文章下面的评论~
⑥ 实现登陆
可以在颁发 Token 时把能够使用的 API 存储进去,但是这种方法不适合 API 较多的情况。
可以存放 用户信息(Claims)和角色信息,后台通过角色信息获取授权访问的 API 列表。
/// <summary>
/// 登陆
/// </summary>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
/// <returns>Token信息</returns>
[HttpPost("login")]
public JsonResult Login(string username, string password)
{
var user = UserModel.Users.FirstOrDefault(x => x.UserName == username && x.UserPossword == password);
if (user == null)
return new JsonResult(
new ResponseModel
{
Code = 0,
Message = "登陆失败!"
});
// 配置用户标识
var userClaims = new Claim[]
{
new Claim(ClaimTypes.Name,user.UserName),
new Claim(ClaimTypes.Role,user.Role),
new Claim(ClaimTypes.Expiration,DateTime.Now.AddMinutes(_requirement.Expiration.TotalMinutes).ToString()),
};
_requirement.SetRolesName(user.Role);
// 生成用户标识
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
identity.AddClaims(userClaims);
var token = JwtToken.BuildJwtToken(userClaims, _requirement);
return new JsonResult(
new ResponseModel
{
Code = 200,
Message = "登陆成功!请注意保存你的 Token 凭证!",
Data = token
});
}
⑦ 添加 API 授权策略
[Authorize(Policy = "Permission")]
⑧ 实现自定义授权校验
要实现自定义 API 角色/策略授权,需要继承 AuthorizationHandler<TRequirement>
。
里面的内容是完全自定义的, AuthorizationHandlerContext
是认证授权的上下文,在此实现自定义的访问授权认证。
也可以加上自动刷新 Token 的功能。
/// <summary>
/// 验证用户信息,进行权限授权Handler
/// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
List<PermissionRequirement> requirements = new List<PermissionRequirement>();
foreach (var item in context.Requirements)
{
requirements.Add((PermissionRequirement)item);
}
foreach (var item in requirements)
{
// 校验 颁发和接收对象
if (!(item.Issuer == AuthConfig.Issuer ?
item.Audience == AuthConfig.Audience ?
true : false : false))
{
context.Fail();
}
// 校验过期时间
var nowTime = DateTimeOffset.Now.ToUnixTimeSeconds();
var issued = item.IssuedTime +Convert.ToInt64(item.Expiration.TotalSeconds);
if (issued < nowTime)
context.Fail();
// 是否有访问此 API 的权限
var resource = ((Microsoft.AspNetCore.Routing.RouteEndpoint)context.Resource).RoutePattern;
var permissions = item.Roles.Permissions.ToList();
var apis = permissions.Any(x => x.Name.ToLower() == item.Roles.Name.ToLower() && x.Url.ToLower() == resource.RawText.ToLower());
if (!apis)
context.Fail();
context.Succeed(requirement);
// 无权限时跳转到某个页面
//var httpcontext = new HttpContextAccessor();
//httpcontext.HttpContext.Response.Redirect(item.DeniedAction);
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
⑨ 一些有用的代码
将字符串生成哈希值,例如密码。
为了安全,删除字符串里面的特殊字符,例如 "
、'
、$
。
public static class AccountHash
{
// 获取字符串的哈希值
public static string GetByHashString(string str)
{
string hash = GetMd5Hash(str.Replace("\"", String.Empty)
.Replace("\'", String.Empty)
.Replace("$", String.Empty));
return hash;
}
/// <summary>
/// 获取用于加密 Token 的密钥
/// </summary>
/// <returns></returns>
public static SigningCredentials GetTokenSecurityKey()
{
var securityKey = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)), SecurityAlgorithms.HmacSha256);
return securityKey;
}
private static string GetMd5Hash(string source)
{
MD5 md5Hash = MD5.Create();
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(source));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
}
签发 Token
PermissionRequirement
不是必须的,用来存放角色或策略认证信息,Claims 应该是必须的。
/// <summary>
/// 颁发用户Token
/// </summary>
public class JwtToken
{
/// <summary>
/// 获取基于JWT的Token
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
Status = true,
access_token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalMilliseconds,
token_type = "Bearer"
};
return response;
}
表示时间戳
// Unix 时间戳
DateTimeOffset.Now.ToUnixTimeSeconds();
// 检验 Token 是否过期
// 将 TimeSpan 转为 Unix 时间戳
Convert.ToInt64(TimeSpan);
DateTimeOffset.Now.ToUnixTimeSeconds() + Convert.ToInt64(TimeSpan);