net core WebApi——April.Util更新之权限
目录
前言
在之前已经提到过,公用类库util已经开源,目的一是为了简化开发的工作量,毕竟有些常规的功能类库重复率还是挺高的,二是为了一起探讨学习软件开发,用的人越多问题也就会越多,解决的问题越多功能也就越完善,仓库地址: april.util_github,april.util_gitee,还没关注的朋友希望可以先mark,后续会持续维护。
权限
在之前的net core webapi——公用库april.util公开及发布中已经介绍了初次发布的一些功能,其中包括缓存,日志,加密,统一的配置等等,具体可以再回头看下这篇介绍,而在其中有个tokenutil,因为当时发布的时候这块儿还没有更新上,趁着周末来整理下吧。
关于webapi的权限,可以借助identity,jwt,但是我这里没有借助这些,只是自己做了个token的生成已经存储用户主要信息,对于权限我想大多数人已经有了一套自己的权限体系,所以这里我简单介绍下我的思路。
- 首先对于菜单做权限标示,请求的控制器,请求的事件
- 菜单信息维护后,设置角色对应多个菜单
- 管理员对应多个角色
- 在登录的时候根据账号信息获取对应管理员的角色及最终菜单,控制器,事件
- 处理管理员信息后自定义token,可设置token过期时间,token可以反解析(如果到期自动重新授权,我这里没有处理)
- 每次访问接口的时候(除公开不需校验的接口),根据请求的路径判断是否有当前控制器权限(通过中间层),进入接口后判断是否有对应权限(通过标签)
通过上述流程来做权限的校验,当然这里只是针对单应用,如果是多应用的话,这里还要考虑应用问题(如,一个授权认证工程主做身份校验,多个应用工程通用一个管理)。
首先,我们需要一个可以存储管理员的对应属性集合adminentity,主要存储基本信息,控制器集合,权限集合,数据集合(也就是企业部门等)。
/// <summary> /// 管理员实体 /// </summary> public class adminentity { private int _id = -1; private string _username = string.empty; private string _avator = string.empty; private list<string> _controllers = new list<string>(); private list<string> _permissions = new list<string>(); private int _tokentype = 0; private bool _issupermanager = false; private list<int> _depts = new list<int>(); private int _currentdept = -1; private datetime _expiretime = datetime.now; /// <summary> /// 主键 /// </summary> public int id { get => _id; set => _id = value; } /// <summary> /// 用户名 /// </summary> public string username { get => _username; set => _username = value; } /// <summary> /// 头像 /// </summary> public string avator { get => _avator; set => _avator = value; } /// <summary> /// 控制器集合 /// </summary> public list<string> controllers { get => _controllers; set => _controllers = value; } /// <summary> /// 权限集合 /// </summary> public list<string> permissions { get => _permissions; set => _permissions = value; } /// <summary> /// 访问方式 /// </summary> public int tokentype { get => _tokentype; set => _tokentype = value; } /// <summary> /// 是否为超管 /// </summary> public bool issupermanager { get => _issupermanager; set => _issupermanager = value; } /// <summary> /// 企业集合 /// </summary> public list<int> depts { get => _depts; set => _depts = value; } /// <summary> /// 当前企业 /// </summary> public int currentdept { get => _currentdept; set => _currentdept = value; } /// <summary> /// 过期时间 /// </summary> public datetime expiretime { get => _expiretime; set => _expiretime = value; } }
之后我们来完成tokenutil这块儿,首先是生成我们的token串,因为考虑到需要反解析,所以这里采用的是字符串加解密,当然这个加密串具体是什么可以自定义,目前我这里设置的是固定需要两个参数{id},{ts},目的是为了保证加密串的唯一,当然也是为了过期无感知重新授权准备的。
public class tokenutil { /// <summary> /// 设置token /// </summary> /// <returns></returns> public static string gettoken(adminentity user, out string expiretimstamp) { string id = user.id.tostring(); double exp = 0; switch ((aprilenums.tokentype)user.tokentype) { case aprilenums.tokentype.web: exp = aprilconfig.webexpire; break; case aprilenums.tokentype.app: exp = aprilconfig.appexpire; break; case aprilenums.tokentype.miniprogram: exp = aprilconfig.miniprogramexpire; break; case aprilenums.tokentype.other: exp = aprilconfig.otherexpire; break; } datetime date = datetime.now.addhours(exp); user.expiretime = date; double timestamp = dateutil.converttounixtimestamp(date); expiretimstamp = timestamp.tostring(); string token = aprilconfig.tokensecretformat.replace("{id}", id).replace("{ts}", expiretimstamp); token = encryptutil.encryptdes(token, encryptutil.securitykey); //logutil.debug($"用户{id}获取token:{token}"); add(token, user); //处理多点登录 setusertoken(token, user.id); return token; } /// <summary> /// 通过token获取当前人员信息 /// </summary> /// <param name="token"></param> /// <returns></returns> public static adminentity getuserbytoken(string token = "") { if (string.isnullorempty(token)) { token = gettokenbycontent(); } if (!string.isnullorempty(token)) { adminentity admin = get(token); if (admin != null) { //校验时间 if (admin.expiretime > datetime.now) { if (aprilconfig.allowsliding) { //延长时间 admin.expiretime = datetime.now.addminutes(30); //更新 add(token, admin); } return admin; } else { //已经过期的就不再延长了,当然后续根据情况改进吧 return null; } } } return null; } /// <summary> /// 通过用户请求信息获取token信息 /// </summary> /// <returns></returns> public static string gettokenbycontent() { string token = ""; //判断header var headers = aprilconfig.httpcurrent.request.headers; if (headers.containskey("token")) { token = headers["token"].tostring(); } if (string.isnullorempty(token)) { token = cookieutil.getstring("token"); } if (string.isnullorempty(token)) { aprilconfig.httpcurrent.request.query.trygetvalue("token", out stringvalues temptoken); if (temptoken != stringvalues.empty) { token = temptoken.tostring(); } } return token; } /// <summary> /// 移除token /// </summary> /// <param name="token"></param> public static void removetoken(string token = "") { if (string.isnullorempty(token)) { token = gettokenbycontent(); } if (!string.isnullorempty(token)) { remove(token); } } #region 多个登录 /// <summary> /// 多个登录设置缓存 /// </summary> /// <param name="token"></param> /// <param name="userid"></param> public static void setusertoken(string token, int userid) { dictionary<int, list<string>> dicusers = cacheutil.get<dictionary<int, list<string>>>("usertoken"); if (dicusers == null) { dicusers = new dictionary<int, list<string>>(); } list<string> listtokens = new list<string>(); if (dicusers.containskey(userid)) { listtokens = dicusers[userid]; if (listtokens.count <= 0) { listtokens.add(token); } else { if (!aprilconfig.allowmuiltilogin) { foreach (var item in listtokens) { removetoken(item); } listtokens.add(token); } else { bool isadd = true; foreach (var item in listtokens) { if (item == token) { isadd = false; } } if (isadd) { listtokens.add(token); } } } } else { listtokens.add(token); dicusers.add(userid, listtokens); } cacheutil.add("usertoken", dicusers, new timespan(6, 0, 0), true); } /// <summary> /// 多个登录删除缓存 /// </summary> /// <param name="userid"></param> public static void removeusertoken(int userid) { dictionary<int, list<string>> dicusers = cacheutil.get<dictionary<int, list<string>>>("usertoken"); if (dicusers != null && dicusers.count > 0) { if (dicusers.containskey(userid)) { //删除所有token var listtokens = dicusers[userid]; foreach (var token in listtokens) { removetoken(token); } dicusers.remove(userid); } } } /// <summary> /// 多个登录获取 /// </summary> /// <param name="userid"></param> /// <returns></returns> public static list<string> getusertoken(int userid) { dictionary<int, list<string>> dicusers = cacheutil.get<dictionary<int, list<string>>>("usertoken"); list<string> lists = new list<string>(); if (dicusers != null && dicusers.count > 0) { foreach (var item in dicusers) { if (item.key == userid) { lists = dicusers[userid]; break; } } } return lists; } #endregion #region 私有方法(这块儿还需要改进) private static void add(string token,adminentity admin) { switch (aprilconfig.tokencachetype) { //不推荐cookie case aprilenums.tokencachetype.cookie: cookieutil.add(token, admin); break; case aprilenums.tokencachetype.cache: cacheutil.add(token, admin, new timespan(0, 30, 0)); break; case aprilenums.tokencachetype.session: sessionutil.add(token, admin); break; case aprilenums.tokencachetype.redis: redisutil.add(token, admin); break; } } private static adminentity get(string token) { adminentity admin = null; switch (aprilconfig.tokencachetype) { case aprilenums.tokencachetype.cookie: admin = cookieutil.get<adminentity>(token); break; case aprilenums.tokencachetype.cache: admin = cacheutil.get<adminentity>(token); break; case aprilenums.tokencachetype.session: admin = sessionutil.get<adminentity>(token); break; case aprilenums.tokencachetype.redis: admin = redisutil.get<adminentity>(token); break; } return admin; } private static void remove(string token) { switch (aprilconfig.tokencachetype) { case aprilenums.tokencachetype.cookie: cookieutil.remove(token); break; case aprilenums.tokencachetype.cache: cacheutil.remove(token); break; case aprilenums.tokencachetype.session: sessionutil.remove(token); break; case aprilenums.tokencachetype.redis: redisutil.remove(token); break; } } #endregion }
中间层
当然这也在之前已经提到过net core webapi基础工程搭建(七)——小试aop及常规测试_part 1,当时还觉得这个叫做拦截器,too young too simple,至于使用方法这里就不多说了,可以参考之前2.2版本的东西,也可以看代码仓库中的示例工程。
public class aprilauthorizationmiddleware { private readonly requestdelegate next; public aprilauthorizationmiddleware(requestdelegate next) { this.next = next; } public task invoke(httpcontext context) { if (context.request.method != "options") { string path = context.request.path.value; if (!aprilconfig.allowurl.contains(path)) { //获取管理员信息 adminentity admin = tokenutil.getuserbytoken(); if (admin == null) { //重新登录 return responseutil.handleresponse(-2, "未登录"); } if (!admin.issupermanager) { //格式统一为/api/controller/action,兼容多级如/api/controller1/conrolerinnername/xxx/action string[] strvalues = system.text.regularexpressions.regex.split(path, "/"); string controller = ""; bool isstartapi = false; if (path.startswith("/api")) { isstartapi = true; } for (int i = 0; i < strvalues.length; i++) { //为空,为api,或者最后一个 if (string.isnullorempty(strvalues[i]) || i == strvalues.length - 1) { continue; } if (isstartapi && strvalues[i] == "api") { continue; } if (!string.isnullorempty(controller)) { controller += "/"; } controller += strvalues[i]; } if (string.isnullorempty(controller)) { controller = strvalues[strvalues.length - 1]; } if (!admin.controllers.contains(controller.tolower())) { //无权访问 return responseutil.handleresponse(401, "无权访问"); } } } } return next.invoke(context); } }
ok,我们先来看下login中的操作以及实现效果吧。
[httppost] public async task<responsedataentity> login(loginformentity formentity) { if (string.isnullorempty(formentity.loginname) || string.isnullorempty(formentity.password)) { return responseutil.fail("请输入账号密码"); } if (formentity.loginname == "admin") { //这里实际应该通过db获取管理员 string password = encryptutil.md5encrypt(formentity.password, aprilconfig.securitykey); if (password == "b092956160cb0018") { //获取管理员相关权限,同样是db获取,这里只做展示 adminentity admin = new adminentity { username = "超级管理员", avator = "", issupermanager = true, tokentype = (int)aprilenums.tokentype.web }; string token = tokenutil.gettoken(admin, out string expiretimestamp); int expiretime = 0; int.tryparse(expiretimestamp, out expiretime); //可以考虑记录登录日志等其他信息 return responseutil.success("", new { username = admin.username, avator = admin.avator, token = token, expire = expiretime }); } } else if (formentity.loginname == "test") { //这里做权限演示 adminentity admin = new adminentity { username = "测试", avator = "", tokentype = (int)aprilenums.tokentype.web }; admin.controllers.add("weatherforecast"); admin.permissions.add("weatherforecast_log");//控制器_事件(add,update...) string token = tokenutil.gettoken(admin, out string expiretimestamp); int expiretime = 0; int.tryparse(expiretimestamp, out expiretime); //可以考虑记录登录日志等其他信息 return responseutil.success("", new { username = admin.username, avator = admin.avator, token = token, expire = expiretime }); } //这里其实已经可以考虑验证码相关了,但是这是示例工程,后续可持续关注我,会有基础工程(带权限)的实例公开 return responseutil.fail("账号密码错误"); }
可能乍一看会先吐槽下,明明是异步接口还用同步的方法,没有异步的实现空浪费内存xxx,因为db考虑是要搞异步,所以这里示例就这样先写了,主要是领会精神,咳咳。
来试下效果吧,首先我们随便访问个白名单外的接口。
然后我们通过账号登陆login接口(直接写死了,admin,123456),获取到token。
然后我们来访问接口。
是不是还是未登录,没错,因为没有token的传值,当然我这里是通过query传值,支持header,token,query。
这里因为是超管,所以权限随意搞,无所谓,接下来展示下普通用户的权限标示。
目前可以通过标签aprilpermission,把当前的控制器与对应事件的权限作为参数传递,之后根据当前管理员信息做校验。
public class aprilpermissionattribute : attribute, iactionfilter { public string permission; public string controller; /// <summary> /// 构造函数 /// </summary> /// <param name="_controller">控制器</param> /// <param name="_permission">接口事件</param> public aprilpermissionattribute(string _controller, string _permission) { permission = _permission; controller = _controller; } public void onactionexecuted(actionexecutedcontext context) { logutil.debug("aprilpermission onactionexecuted"); } public void onactionexecuting(actionexecutingcontext context) { adminentity admin = tokenutil.getuserbytoken(); if (admin == null || admin.expiretime <= datetime.now) { context.result = new objectresult(new { msg = "未登录", code = -2 }); } if (!admin.issupermanager) { string controller_permission = $"{controller}_{permission}"; if (!admin.controllers.contains(controller) || !admin.permissions.contains(controller_permission)) { context.result = new objectresult(new { msg = "无权访问", code = 401 }); } } } }
针对几个接口做了调整,附上标签后判断权限,我们来测试下登录test,密码随意。
至此权限相关的功能也统一起来,当然如果有个性化的还是需要调整的,后续也是会不断的更新改动。
小结
权限还是稍微麻烦点儿啊,通过中间层,标签以及tokenutil来完成登录授权这块儿,至于数据的划分,毕竟这个东西不是通用的,所以只是点出来而没有去整合,如果有好的建议或者自己整合的通用类库也可以跟我交流。
推荐阅读
-
abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之五(三十一)
-
abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之八(三十四)
-
net core WebApi——公用库April.Util公开及发布
-
net core WebApi——April.Util更新之权限
-
abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之四(三十)
-
asp.net core 系列之webapi集成Dapper的简单操作教程
-
asp.net core 系列之webapi集成EFCore的简单操作教程
-
.NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程
-
ASP.NET WebApi总结之自定义权限验证
-
abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之七(三十三)