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来完成登录授权这块儿,至于数据的划分,毕竟这个东西不是通用的,所以只是点出来而没有去整合,如果有好的建议或者自己整合的通用类库也可以跟我交流。
net core WebApi——April.Util更新之权限的更多相关文章
- April.Util更新之权限
目录 前言 权限 中间层 小结 前言 在之前已经提到过,公用类库Util已经开源,目的一是为了简化开发的工作量,毕竟有些常规的功能类库重复率还是挺高的,二是为了一起探讨学习软件开发,用的人越多问题也就 ...
- net core WebApi——公用库April.Util公开及发布
前言 在之前鼓捣过一次基础工程April.WebApi后,就考虑把常用的类库打包做成一个公共类库,这样既方便维护也方便后续做快速开发使用,仓库地址:April.Util_github,April.Ut ...
- SAAS云平台搭建札记: (三) AntDesign + .Net Core WebAPI权限控制、动态菜单的生成
我们知道,当下最火的前端框架,非蚂蚁金服的AntDesign莫属,这个框架不仅在国内非常有名,在国外GitHub上React前端框架也排名第一.而且这个框架涵盖了React.Vue.Angular等多 ...
- net core WebApi——缓存神器Redis
目录 前言 Redis 使用 RedisUtil 测试 小结 @ 前言 中秋过完不知不觉都已经快两周没动这个工程了,最近业务需要总算开始搞后台云服务了,果断直接net core搞起,在做的中间遇到了不 ...
- .Net Core应用框架Util介绍(五)
上篇简要介绍了Util在Angular Ts方面的封装情况,本文介绍Angular封装的另一个部分,即Html的封装. 标准组件与业务组件 对于管理后台这样的表单系统,你通常会使用Angular Ma ...
- 零基础ASP.NET Core WebAPI团队协作开发
零基础ASP.NET Core WebAPI团队协作开发 相信大家对“前后端分离”和“微服务”这两个词应该是耳熟能详了.网上也有很多介绍这方面的文章,写的都很好.我这里提这个是因为接下来我要分享的内容 ...
- net core Webapi基础工程搭建(七)——小试AOP及常规测试_Part 2
目录 前言 引入 自定义属性 测试 小结 前言 前一篇讲到了中间层的使用,可能不是那么AOP,今天主要来说下一个轻量级的AOP第三方类库AspectoCore. 简单介绍下这个类库,AspectCor ...
- net core Webapi基础工程搭建(六)——数据库操作_Part 2
目录 前言 开始 使用 小结 前言 昨天是写着写着发现,时间不早了,已经养成了晚上下班抽时间看看能写点儿啥的习惯(貌似),今天实在是不想让昨天没做完的事情影响,所以又坐下,沉下心(周末了),开始把数据 ...
- net core Webapi基础工程搭建(六)——数据库操作_Part 1
目录 前言 SqlSugar Service层 BaseService(基类) 小结 前言 后端开发最常打交道的就是数据库了(静态网站靠边),上一篇net core Webapi基础工程搭建(五)-- ...
随机推荐
- linux下安装配置go语言环境
1,golang中国下载go源码 http://www.golangtc.com/download 请对应系统版本号,linux-amd64.tar.gz为64位系统(推荐) ,linux-386 ...
- bat脚本自动安装Jmeter&Jdk
一句话能解决的事情,绝对不要写一篇文章:一篇文章能解决的事情,绝对不要使用各种工具:一个工具能解决的事情,绝对不要跑东跑西…… 文章主要介绍脚本如何下载.安装.配置Jmeter&Jdk. 不多 ...
- JavaScript设计模式——原型模式
原型模式: 原型模式是指原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象,是一种用来创建对象的模式,也就是创建一个对象作为另一个对象的prototype属性: prototype警告:学习了 ...
- SpringBootSecurity学习(23)前后端分离版之OAuth2.0 其它模式
密码模式 前面介绍了授权码模式和刷新令牌两种获取最新令牌的方法,下面来看一下其它模式.首先看密码模式,我们默认配置的三种模式中其实就包含密码模式的支持: 因此我们启动项目,直接使用密码模式即可,访问地 ...
- 初探内核之《Linux内核设计与实现》笔记下
定时器和时间管理 系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于linux来说非常重要. 主要内容: 系统时间 定时器 定时器相关 ...
- SpringCloud之Eureka、Ribbon
一.微服务架构 简单的说,微服务是系统架构的一种设计风格,它的主旨是将一个原本独立的系统拆分为多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通 ...
- 基于STM32F429和HAL库的CAN收发例程
1.CAN协议介绍 CAN 是 Controller Area Network 的缩写(以下称为 CAN),是 ISO 国际标准化的串行通信协议.在当前的汽车产业中,出于对安全性.舒适性.方便性.低公 ...
- 用 Python 爬取网易严选妹子内衣信息,探究妹纸们的偏好
网易商品评论爬取 分析网页 评论分析 进入到网易精选官网,搜索“文胸”后,先随便点进一个商品. 在商品页面,打开 Chrome 的控制台,切换至 Network 页,再把商品页Python入门到精通学 ...
- Vue-CLI 项目在pycharm中配置
Vue-CLI Vue-CLI 项目在pycharm中配置 第一步 pycharm索引到vue项目的根目录,打开 第二步 安装vue.js插件来高亮 .vue 文件代码(见插图) 第三步 第四步 配置 ...
- session与cookie,django中间件
0819自我总结 一.session与cookie 1.django设置session request.session['name'] = username request.session['age' ...