您的位置:首页 > 其它

从壹开始前后端分离[.NetCore] 37 ║JWT完美实现权限与接口的动态分配

2018-12-19 13:00 876 查看

缘起

哈喽大家好呀!又过去一周啦,这些天小伙伴们有没有学习呀,已经有一周没有更新文章了,不过晚上的时候,我也会看一些书和资料,这里给大家分享下:

1、之前简单的写了一个DDD+CQRS+ES的第二个系列《D3模式设计初探 与 我的计划书》,已经基本完结了,写的比较简单,然后我也找到了微软的一个官方的一个资料《CQRS Journey》,不知道有没有哪位小伙伴看,全英文的,我还在看,因为官方已经不翻译了,所以我打算自己翻译下,如果有想和我一起的小伙伴,可以留言,咱们成立一个小组,一起翻译这个资料,主要是关于CQRS读写分离的和ES事件溯源的,当然是基于DDD领域驱动设计架构的基础上,有助于自己的理解。

2、然后就是在看《IdentityServer4.Samples》,是一个IS4的官方栗子,然后找了找资料,因为我的第三个系列教程 —— 是想做一个权限管理系统(这里是打算搭建一个 IdentityServer4 + .net core Identity + EFCore + VueAdmin )的一个前后端分离的权限管理框架,初步打算是基于按钮级别的权限控制,从部门到岗位,角色到用户等都动态可控(保存到数据库,可以管理后台修改),开笔时间还没有定,因为还在学习和年底公司总结了,如果有小伙伴想一起开发,可以看看上边的这些技术,咱们以后可以合作,为.net 开源社区做贡献(这里说下是完全无偿的哟)。

好啦,废话不多说,因为今天是不定期更新系列,所以会之间进入主题,不会有概念性的讲解,马上开始今天的内容啦!主要是以下几个方面:

1、实现角色和接口API保存到数据库,实现动态分配

2、接口中,之前的授权方法依然保留,在 BlogController.cs 中还是使用的基于角色的授权方式。

3、本文是 IS4 系列的铺垫文章。

 

一、JWT授权验证,我们经历了哪些

看过我写的这个第一个系列《前后端分离》的小伙伴都知道,我用到了JWT来实现的权限验证,目前已经达到什么程度的验证了呢,这里我经历了三个步骤:

1、直接在 Api 接口地址上设计 Roles 信息

这个也是最简单,最粗暴的方法,直接这么配置

/// <summary>
/// Values控制器
/// </summary>
[Route("api/[controller]")]
[ApiController]
[Authorize(Roles = "Admin,Client")]
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Client")]
[Authorize(Roles = "Other")]
public class ValuesController : ControllerBase
{

}

虽然我们把 用户信息 和 角色Rols信息 保存到了数据库,实现了动态化,但是具体授权的时候,还是需要手动在API接口地址上写特定的Role权限,这样才能对其进行匹配和授权,如果真的有一个接口可以被多个角色访问,那就需要垒了很多了,不是很好。

 

2、对不同模块的角色们 建立策略

鉴于上边的问题,我考虑着对不同的角色建立不同的策略,并在 Startup.cs 启动类中,配置服务:

services.AddAuthorization(options =>
{
options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());

options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());

options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));

options.AddPolicy("SystemOrAdminOrOther", policy => policy.RequireRole("Admin", "System", "Other"));

})

然后在我们的接口api上,只需要写上策略的名称即可:

 

相信大家也都是这么做的,当然我之前也是这么写的。虽然我们在启动类 Startup.cs 中,对我们的Roles做了策略,看起来不用一一的配置 Roles 了,但是大家会发现,好像这个功能并没有想象中那么美丽,因为最关键的问题,我们没有解决,因为这样我们还是需要手动一个接口一个接口的写权限策略,不灵活!我也是想了很久,才想到了今天的这个办法(请耐心往下看)。

 

3、将接口地址和角色授权分离

当然上边的方法也能实现我们的小需要,每个接口一个个都写好即可,但是作为强迫症的我,总感觉会有办法可以把 API 接口,和 Role 权限剥离开,也能像用户和 Role那样,保存到数据库,实现动态分配,就这样我研究了微软的官方文档,偶然发现了微软官方文档的《Policy-based authorization》基于策略的授权,正好也找到了博客园一个大佬写的文章,我就使用了,这里注明下:借稿作者:《asp.net core 2.0 web api基于JWT自定义策略授权》。

然后我在他的基础上,配合着咱们的项目,做了调整,经过测试,完美的解决了咱们的问题,可以动态的数据库进行配置,那具体是怎么实现的呢,请往下看。

 

二、接口地址和角色保存到数据库

数据库设计不好,大家看我写的思路即可,自己可以做扩展和优化,希望还是自己动手。

 既然要实现动态绑定,我们就需要把接口地址信息、角色信息保存到数据库,那表结构是怎样的呢,其实目前我的数据库结构已经可以满足了要求了,只不过需要稍微调整下,因为之前我是用EF来设计的,这里用SqlSugar会出现一个问题,所以需要在 Blog.Core.Model 层引用 sqlSugarCore 的 Nuget 包,然后把实体 RoleModulePermission.cs 中的三个参数做下忽略处理。

1、实体模型设计

首先是接口和角色的关联表的实体模型:

namespace Blog.Core.Model.Models
{
/// <summary>
/// 接口、角色关联表(以后可以把按钮设计进来)
/// </summary>
public class RoleModulePermission
{
public int Id { get; set; }
/// <summary>
/// 角色ID
/// </summary>
public int RoleId { get; set; }
/// <summary>
/// 菜单ID,这里就是api地址的信息
/// </summary>
public int ModuleId { get; set; }
/// <summary>
/// 按钮ID
/// </summary>
public int? PermissionId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreateTime { get; set; }
/// <summary>
///获取或设置是否禁用,逻辑上的删除,非物理删除
/// </summary>
public bool? IsDeleted { get; set; }

// 等等,还有其他属性,其他的可以参考Code,或者自定义...

// 请注意,下边三个实体参数,只是做传参作用,所以忽略下,不然会认为缺少字段
[SugarColumn(IsIgnore = true)]
public virtual Role Role { get; set; }
[SugarColumn(IsIgnore = true)]
public virtual Module Module { get; set; }
[SugarColumn(IsIgnore = true)]
public virtual Permission Permission { get; set; }
}
}

 

然后就是API接口信息保存的实体模型:

namespace Blog.Core.Model.Models
{
/// <summary>
/// 接口API地址信息表
/// </summary>
public class Module
{
public int Id { get; set; }
/// <summary>
/// 父ID
/// </summary>
public int? ParentId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// API链接地址
/// </summary>
public string LinkUrl { get; set; }
/// <summary>
/// 控制器名称
/// </summary>
public string Controller { get; set; }
/// <summary>
/// Action名称
/// </summary>
public string Action { get; set; }
/// <summary>
/// 图标
/// </summary>
public string Icon { get; set; }
/// <summary>
/// 菜单编号
/// </summary>
public string Code { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderSort { get; set; }
/// <summary>
/// /描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 是否激活
/// </summary>
public bool Enabled { get; set; }

// 等等其他属性,具体的可以看我的Code,或者自己自定义...
}
}

 

2、Service 应用服务接口设计

这个很简单,CURD中,我只是简单写了一个查询全部关系的接口,其他的都很简单,相信自己也能搞定,IRepository.cs 、Repository.cs 和 IServices.cs 这三个我就不多写了,简单看下 Services.cs 的一个查询全部角色接口关系的方法:

namespace Blog.Core.Services
{
/// <summary>
/// RoleModulePermissionServices 应用服务
/// </summary>
public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
{

IRoleModulePermissionRepository dal;
IModuleRepository moduleRepository;
IRoleRepository roleRepository;

// 将多个仓储接口注入
public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
{
this.dal = dal;
this.moduleRepository = moduleRepository;
this.roleRepository = roleRepository;
base.baseDal = dal;
}

/// <summary>
/// 获取全部 角色接口(按钮)关系数据 注意我使用咱们之前的AOP缓存,很好的应用上了
/// </summary>
/// <returns></returns>
[Caching(AbsoluteExpiration = 10)]
public async Task<List<RoleModulePermission>> GeRoleModule()
{
var roleModulePermissions = await dal.Query(a => a.IsDeleted == false);
if (roleModulePermissions.Count > 0)
{
foreach (var item in roleModulePermissions)
{
item.Role = await roleRepository.QueryByID(item.RoleId);
item.Module = await moduleRepository.QueryByID(item.ModuleId);
}

}
return roleModulePermissions;
}

}
}

 

我自己简单的设计了下数据,如果有想我的数据的,请留言,我把这个Sql数据文件放到 Github :

这里设计使用外键,多对多的形式,可以很好的实现扩展,比如接口地址API变了,但是我们使用的是id,可以很灵活的适应改变。

 

 

三、基于策略授权的自定义验证——核心

之前咱们也使用过中间件 JwtTokenAuth 来进行授权验证,后来因为过期时间的问题,然后使用的官方的中间件app.UseAuthentication() ,今天咱们就写一个3.0版本的验证方法,基于AuthorizationHandler 的权限授权处理器,具体的请往下看,如果看不懂,可以直接 pull 下我的 Github 代码即可。

一共是四个类:

 

1、JwtToken 生成令牌

这个很简单,就是我们之前的 Token 字符串生成类,这里不过多做解释,只是要注意一下下边红色的参数 PermissionRequirement ,数据是从Startup.cs 中注入的,下边会说到。

namespace Blog.Core.AuthHelper
{
/// <summary>
/// JWTToken生成类
/// </summary>
public class JwtToken
{
/// <summary>
/// 获取基于JWT的Token
/// </summary>
/// <param name="claims">需要在登陆的时候配置</param>
/// <param name="permissionRequirement">在startup中定义的参数</param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.Now;
// 实例化JwtSecurityToken
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
// 生成 Token
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

//打包返回前台
var responseJson = new
{
success = true,
token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalSeconds,
token_type = "Bearer"
};
return responseJson;
}
}
}

 

2、Permission 凭据实体

说白了,这个就是用来存放我们用户登录成果后,在httptext中存放的角色信息的,是下边 必要参数类 PermissionRequirement 的一个属性,很简单,不细说:

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

 

3、PermissionRequirement 令牌必要参数类

这里边存放的都是 Jwt Token 的全部信息,注意它继承了 IAuthorizationRequirement,因为我们要设计自定义授权验证处理器,所以必须继承验证要求接口,才能设计我们自己的参数:

namespace Blog.Core.AuthHelper
{
/// <summary>
/// 必要参数类,
/// 继承 IAuthorizationRequirement,用于设计自定义权限处理器PermissionHandler
/// 因为AuthorizationHandler 中的泛型参数 TRequirement 必须继承 IAuthorizationRequirement
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// 用户权限集合
/// </summary>
public List<Permission> Permissions { get; 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; }
/// <summary>
/// 签名验证
/// </summary>
public SigningCredentials SigningCredentials { get; set; }

/// <summary>
/// 构造
/// </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>
/// <param name="expiration">过期时间</param>
public PermissionRequirement(string deniedAction, List<Permission> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration)
{
ClaimType = claimType;
DeniedAction = deniedAction;
Permissions = permissions;
Issuer = issuer;
Audience = audience;
Expiration = expiration;
SigningCredentials = signingCredentials;
}
}
}

 

4、PermissionHandler 自定义授权处理器,核心!

 我们先看代码:

namespace Blog.Core.AuthHelper
{
/// <summary>
/// 权限授权处理器 继承AuthorizationHandler ,并且需要一个权限必要参数
/// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
/// <summary>
/// 验证方案提供对象
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; }

/// <summary>
/// services 层注入
/// </summary>
public IRoleModulePermissionServices _roleModulePermissionServices { get; set; }

/// <summary>
/// 构造函数注入
/// </summary>
/// <param name="schemes"></param>
/// <param name="roleModulePermissionServices"></param>
public PermissionHandler(IAuthenticationSchemeProvider schemes, IRoleModulePermissionServices roleModulePermissionServices)
{
Schemes = schemes;
_roleModulePermissionServices = roleModulePermissionServices;
}

// 重载异步处理程序
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
// 将最新的角色和接口列表更新,
// 注意这里我用到了AOP缓存,只是减少与数据库的访问次数,而又保证是最新的数据
var data = await _roleModulePermissionServices.GeRoleModule(); var list = (from item in data where item.IsDeleted == false orderby item.Id select new Permission { Url = item.Module?.LinkUrl, Role = item.Role?.Name, }).ToList(); requirement.Permissions = list; //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //请求Url var questUrl = httpContext.Request.Path.Value.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不为空即登录成功 if (result?.Principal != null) { httpContext.User = result.Principal; //权限中是否存在请求的url if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0) { // 获取当前用户的角色信息 var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //验证权限 if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role) && w.Url.ToLower() == questUrl).Count() <= 0) { context.Fail(); return; // 可以在这里设置跳转页面,不过还是会访问当前接口地址的 httpContext.Response.Redirect(requirement.DeniedAction); } } else { context.Fail(); return; } //判断过期时间 if ((httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) != null && DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now) { context.Succeed(requirement); } else { context.Fail(); return; } return; } } //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)) { context.Fail(); return; } context.Succeed(requirement); } } }

 

基本的解释上边已经写了,应该能看懂,这里只有一点,就是我们自定义的这个处理器,是继承了AuthorizationHandler ,而且它还需要一个泛型类,并且该泛型类必须继承IAuthorizationRequirement 这个授权要求的接口,这样我们就可以很方便的把我们的自定义的权限参数传入授权处理器中。

 

好啦,到了这里,我们已经设计好了处理器,那如何配置在启动服务中呢,请继续看。

 

四、配置授权服务与使用

 这里主要是在我们的启动类 Startup.cs 中的服务配置,其实和之前的差不多,只是做了简单的封装,大家一定都能看的懂:

1、将JWT密钥等信息封装到配置文件

在接口层的 appsettings.json 文件中,配置我们的jwt令牌信息:

"Audience": {
"Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs",
"Issuer": "Blog.Core",
"Audience": "wr"
}

 

2、修改JWT服务注册方法

在启动类 Startup.cs 中的服务方法ConfigureServices 中,修改我们的JWT Token 服务注册方法:

      #region JWT Token Service
//读取配置文件
var audienceConfig = Configuration.GetSection("Audience");
var symmetricKeyAsBase64 = audienceConfig["Secret"];
var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
var signingKey = new SymmetricSecurityKey(keyByteArray);

// 令牌验证参数,之前我们都是写在AddJwtBearer里的,这里提出来了
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,//验证发行人的签名密钥
IssuerSigningKey = signingKey,
ValidateIssuer = true,//验证发行人
ValidIssuer = audienceConfig["Issuer"],//发行人
ValidateAudience = true,//验证订阅人
ValidAudience = audienceConfig["Audience"],//订阅人
ValidateLifetime = true,//验证生命周期
ClockSkew = TimeSpan.Zero,//这个是定义的过期的缓存时间
RequireExpirationTime = true,//是否要求过期

};
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

// 注意使用RESTful风格的接口会更好,因为只需要写一个Url即可,比如:/api/values 代表了Get Post Put Delete等多个。
// 如果想写死,可以直接在这里写。
//var permission = new List<Permission> {
//                  new Permission {  Url="/api/values", Role="Admin"},
//                  new Permission {  Url="/api/values", Role="System"},
//                  new Permission {  Url="/api/claims", Role="Admin"},
//              };

// 如果要数据库动态绑定,这里先留个空,后边处理器里动态赋值
var permission = new List<Permission>();

// 角色与接口的权限要求参数
var permissionRequirement = new PermissionRequirement(
"/api/denied",// 拒绝授权的跳转地址(目前无用)
permission,//这里还记得么,就是我们上边说到的角色地址信息凭据实体类 Permission
ClaimTypes.Role,//基于角色的授权
audienceConfig["Issuer"],//发行人
audienceConfig["Audience"],//订阅人
signingCredentials,//签名凭据
expiration: TimeSpan.FromSeconds(60*2)//接口的过期时间,注意这里没有了缓冲时间,你也可以自定义,在上边的TokenValidationParameters的 ClockSkew
);

services.AddAuthorization(options =>
{
options.AddPolicy("Client",
policy => policy.RequireRole("Client").Build());
options.AddPolicy("Admin",
policy => policy.RequireRole("Admin").Build());
options.AddPolicy("SystemOrAdmin",
policy => policy.RequireRole("Admin", "System"));

// 自定义基于策略的授权权限
options.AddPolicy("Permission",
policy => policy.Requirements.Add(permissionRequirement));
})

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

.AddJwtBearer(o =>
{
o.TokenValidationParameters = tokenValidationParameters;
});

// 依赖注入,将自定义的授权处理器 匹配给官方授权处理器接口,这样当系统处理授权的时候,就会直接访问我们自定义的授权处理器了。
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
       // 将授权必要类注入生命周期内 services.AddSingleton(permissionRequirement); #endregion

 

3、在接口中很方便调用

这样定义好以后,我们只需要很方便的在每一个controller上边写上 [Authorize("Permission")],这个验证特性即可,这个名字就是我们的策略名,我们就不用再想哪一个接口对应哪些Roles了,是不是更方便了!当然如果不写这个特性的话,不会被限制,比如那些前台的页面接口,就不需要被限制。

 

 

4、使用效果展示

咱们看看平时会遇到的4种情况。

1、接口没有配置权限

这种情况,无论是数据库是否配置,都会很正常的通过HTTP请求,从而获取到我们的数据,就比如登录页:

 

2、接口设置了权限,但是数据库没有配置

咱们以 ValuesController 为例子

 

 现在我们把API接口是 /api/values 的接口和角色关联的表给逻辑删除了,那这个时候,也就代表了,当前接口虽然设置了权限,但是在数据库里并没有配置它与Role的关系:

 

那如果我们访问的话会是怎样:

 

我们看到了报错403,当然你也可以自定义错误,就是在 PermissionHandler.cs 自定义权限授权处理程序里,可以自己扩展。

 

3、接口设置了权限,并且数据库也配置了

还是使用咱们的 ValueController.cs ,这时候咱们把刚刚逻辑删除的改成False:

 

 

然后看看我们的执行过程:

 

 

最后经过了两分钟,令牌过期:

 

好啦,这些简单的授权功能已经够咱们使用了,还能在数据库里动态配置,不是么?

 

五、思考

到这里,咱们的这个项目已经完全能实现权限的动态分配了,当然这里是没有后台界面的,你可以自己建立一个MVC项目来实验,也可以建立一个Vue管理后台来分配,都是很简单的,我个人感觉已经很完美了,咱们的项目基本也成型了。

但是这些都是咱们自己造的轮子,那如果我们用一直很高调的 IdentityServer4 这个已经成熟的轮子来实现接口级别的动态授权是怎么做呢?

请看我的下一个系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提前祝大家圣诞节快乐!

 

 

六、Github & Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

 

-- END

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐