Decorative image frame

D-107

3wly 项目开发组自留地

net core WebApi——April.Util更新之权限

前言

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

权限

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

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

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

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

首先,我们需要一个可以存储管理员的对应属性集合AdminEntity,主要存储基本信息,控制器集合,权限集合,数据集合(也就是企业部门等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// <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},目的是为了保证加密串的唯一,当然也是为了过期无感知重新授权准备的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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版本的东西,也可以看代码仓库中的示例工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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中的操作以及实现效果吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[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,把当前的控制器与对应事件的权限作为参数传递,之后根据当前管理员信息做校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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来完成登录授权这块儿,至于数据的划分,毕竟这个东西不是通用的,所以只是点出来而没有去整合,如果有好的建议或者自己整合的通用类库也可以跟我交流。