目录

前言

在之前已经提到过,公用类库util已经开源,目的一是为了简化开发的工作量,毕竟有些常规的功能类库重复率还是挺高的,二是为了一起探讨学习软件开发,用的人越多问题也就会越多,解决的问题越多功能也就越完善,仓库地址: april.util_github,april.util_gitee,还没关注的朋友希望可以先mark,后续会持续维护。

权限

在之前的net core webapi——公用库april.util公开及发布中已经介绍了初次发布的一些功能,其中包括缓存,日志,加密,统一的配置等等,具体可以再回头看下这篇介绍,而在其中有个tokenutil,因为当时发布的时候这块儿还没有更新上,趁着周末来整理下吧。

关于webapi的权限,可以借助identity,jwt,但是我这里没有借助这些,只是自己做了个token的生成已经存储用户主要信息,对于权限我想大多数人已经有了一套自己的权限体系,所以这里我简单介绍下我的思路。

  1. 首先对于菜单做权限标示,请求的控制器,请求的事件
  2. 菜单信息维护后,设置角色对应多个菜单
  3. 管理员对应多个角色
  4. 在登录的时候根据账号信息获取对应管理员的角色及最终菜单,控制器,事件
  5. 处理管理员信息后自定义token,可设置token过期时间,token可以反解析(如果到期自动重新授权,我这里没有处理)
  6. 每次访问接口的时候(除公开不需校验的接口),根据请求的路径判断是否有当前控制器权限(通过中间层),进入接口后判断是否有对应权限(通过标签)

通过上述流程来做权限的校验,当然这里只是针对单应用,如果是多应用的话,这里还要考虑应用问题(如,一个授权认证工程主做身份校验,多个应用工程通用一个管理)。

首先,我们需要一个可以存储管理员的对应属性集合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来完成登录授权这块儿,至于数据的划分,毕竟这个东西不是通用的,所以只是点出来而没有去整合,如果有好的建议或者自己整合的通用类库也可以跟我交流。