第五节:基于Ocelot网关实现多服务、多级鉴权、方案落地(token/冻结/下线/微服务/Action等)
一. 前言
1 目标需求
(1) 需要实现:token校验(非空、准确性、是否过期)、冻结判断、顶下线判断(单点登录)/手动让token失效
(2) 完整的权限校验
A. 第一层:token的各种校验
B. 第二层:冻结校验
C. 第三层:顶下线校验/手动让token失效校验
D. 第四层:一级鉴权--微服务访问
E. 第五层:二级鉴权--Action的访问
2 整体说明
(1) 服务拆分:网关微服务、授权微服务、大后台微服务、船管微服务、船员公司微服务、交易微服务
(2) 这里为了测试简单,使用Ocelot直接转发,不配置Consul服务注册与发现
(3) 为了测试简单,冻结、解冻、使token失效接口放在Ship_Auth下面,不校验
(4)正常情况下:网关专注于 “是否允许访问微服务”,微服务专注于 “是否允许调用具体接口, 职责分离,避免网关承载的过多逻辑
A 角色对应的一级鉴权,表示微服务层级的权限,可以放在网关处
B 角色对应的二级鉴权,表示微服务下的action权限,可以放在具体的微服务上面。
此处为了测试简单,统一都放在网关位置
3 其他
(1) 服务名称和对应端口
Ship_GateWay: 网关微服务 8100
Ship_Auth: 授权微服务 8001
Ship_BackEnd: 大后台微服务 8002
Ship_CrewCompany: 船员公司微服务 8003
Ship_ManageCompany: 船管微服务 8004
ship_Trade: 交易微服务 8005
(2) 写死的一些信息
用户主键编号:userId=10001
冻结名单Set结构的key:frozenUserList
存放登录用户version的Hash结构的key:userVersionList
用户权限存放String结构的key: user_perList_ + userId
二. 实操
1 思路分析
(1) Token的校验
直接通过代码判断即可
(2) 顶下线判断(单点登录)/手动让token失效
可以采用删除token的方案,也可以采用版本号version自增方案,这里采用version来实现,使用redis中的Hash结构,但是无法区分具体是上述的哪一种
(3) 冻结判断
采用冻结表的方案,可以用DB,这里采用redis的Set结构,冻结的同时需要对token进行处理
(4) 一级鉴权 和 二级鉴权 如何实现 【重点!】
A. 都是采用角色配置权限的模式,获取该用户对应角色的所有权限
B. 以后台微服务权限为例:
一级鉴权形如 ["/Ship_BackEnd/"]
二级鉴权形如 [ "/Ship_BackEnd/Admin/GetAllRoles"] 精确到具体方法了
C. 一级鉴权的时候,请求地址判断开头为 /Ship_BackEnd/ 的时候,就去一级鉴权列表找找有没有 "/Ship_BackEnd/" 即可
D. 二级鉴权的时候,直接使用完整的地址 "/Ship_BackEnd/Admin/GetAllRoles",去二级鉴权列表中是否存在即可
2 授权微服务
(1) 登录接口
查看代码
/// <summary>
/// 登录
/// </summary>
/// <param name="account">账号</param>
/// <param name="pwd">密码</param>
/// <returns></returns>
[HttpPost]
public IActionResult CheckLogin(string account, string pwd)
{
string userId = "10001"; //用户编号
//1.各种校验
//1.1 判断是否冻结
if (RedisHelper.SIsMember("frozenUserList", userId))
{
return Json(new { status = "error", msg = "该用户已被冻结", data = "" });
}
//1.2 判断账号密码的准确性
if (account != "admin" && pwd != "123456")
{
return Json(new { status = "error", msg = "账号或密码不正确", data = "" });
}
//2.从DB中获取角色、权限、即其他信息
List<string> roleList = ["", "", "",]; //获取角色对应的所有权限
List<string> per1 = ["/Ship_Auth/", "/Ship_Auth/Auth/CheckLogin",
"/Ship_Auth/Auth/SetFrozenUser", "/Ship_Auth/Auth/UnFrozenUser", "/Ship_Auth/Auth/SetNoEffectToken"];
List<string> per2 = ["/Ship_BackEnd/", "/Ship_BackEnd/Admin/GetAllRoles"];
List<string> per3 = ["/Ship_CrewCompany/", "/Ship_CrewCompany/ShipCrew/GetCrewList"];
List<string> per4 = ["/Ship_ManageCompany/", "/Ship_ManageCompany/Manage/GetManageInfo"];
List<string> per5 = ["/Ship_Trade/", "/Ship_Trade/Order/GetOrderInfo"];
List<string> permissonList = [.. per1, .. per2, .. per3, .. per4, .. per5]; //根据角色获取权限信息
var key = "user_perList_" + userId;
RedisHelper.Set(key, permissonList);
//3.配置version自增(处理单点登录 或 手动让token过期问题)
long num = RedisHelper.HIncrBy("userVersionList", userId, 1);
//4.生成Token
double exp = (DateTime.UtcNow.AddHours(12) - new DateTime(1970, 1, 1)).TotalSeconds; //12个小时过期
var payload = new Dictionary<string, object> { { "userId", userId }, { "account", account }, { "exp", exp }, { "userVesion", num } };
var token = JWTHelp.JWTJiaM(payload, _config["JWTSecret"]);
return Json(new { status = "ok", msg = "登录成功", data = new { token } });
}
(2) 冻结用户
查看代码
/// <summary>
/// 02-冻结用户
/// </summary>
/// <param name="userId">用户编号</param>
/// <returns></returns>
[HttpPost]
public IActionResult SetFrozenUser(string userId)
{
try
{
RedisHelper.SAdd("frozenUserList", userId);
return Json(new { status = "ok", msg = "冻结成功" });
}
catch (Exception)
{
return Json(new { status = "ok", msg = "冻结失败" });
}
}
(3) 解冻用户
查看代码
/// <summary>
/// 03-解冻用户
/// </summary>
/// <param name="userId">用户编号</param>
/// <returns></returns>
[HttpPost]
public IActionResult UnFrozenUser(string userId)
{
try
{
RedisHelper.SRem("frozenUserList", userId);
return Json(new { status = "ok", msg = "解冻成功" });
}
catch (Exception)
{
return Json(new { status = "ok", msg = "解冻失败" });
}
}
(4) 手动使Token失效
查看代码
/// <summary>
/// 04-手动使Token失效
/// </summary>
/// <param name="userId">用户编号</param>
/// <returns></returns>
[HttpPost]
public IActionResult SetNoEffectToken(string userId)
{
try
{
RedisHelper.HIncrBy("userVersionList", userId, 1); //版本号自增即token失效
return Json(new { status = "ok", msg = "设置成功" });
}
catch (Exception)
{
return Json(new { status = "ok", msg = "设置失败" });
}
}
3 网关微服务
(1) 网关配置文件

{ "Routes": [ // 1. 授权微服务 { //上游:表示Ocelot自身,接收请求的配置 "UpstreamPathTemplate": "/Ship_Auth/{everything}", "UpstreamHttpMethod": [ "Get", "Post" ], //下游:表示转发到目标请求的配置 "DownstreamPathTemplate": "/Api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 8001 } ] }, //2 大后台微服务 { "UpstreamPathTemplate": "/Ship_BackEnd/{everything}", "UpstreamHttpMethod": [ "Get", "Post" ], "DownstreamPathTemplate": "/Api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 8002 } ] }, //3 船员公司微服务 { "UpstreamPathTemplate": "/Ship_CrewCompany/{everything}", "UpstreamHttpMethod": [ "Get", "Post" ], "DownstreamPathTemplate": "/Api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 8003 } ] }, //4 船管微服务 { "UpstreamPathTemplate": "/Ship_ManageCompany/{everything}", "UpstreamHttpMethod": [ "Get", "Post" ], "DownstreamPathTemplate": "/Api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 8004 } ] }, //5 交易微服务 { "UpstreamPathTemplate": "/Ship_Trade/{everything}", "UpstreamHttpMethod": [ "Get", "Post" ], "DownstreamPathTemplate": "/Api/{everything}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 8005 } ] } ], "GlobalConfiguration": { //Consul的全局配置 //"ServiceDiscoveryProvider": { // "Host": "127.0.0.1", // 全局 Consul 服务器地址 // "Port": 8500, // 全局 Consul 服务器端口 // "Type": "Consul" // 全局服务发现类型 //} } }
(2) 各种鉴权中间件

using Ypf_Models; namespace Ship_GateWay; /// <summary> /// 统一鉴权中间件 /// </summary> /// <param name="next"></param> /// <param name="configuration"></param> public class UnifiedAuth_MW(RequestDelegate next, IConfiguration configuration) { private readonly RequestDelegate _next = next; private readonly IConfiguration _configuration = configuration; public async Task Invoke(HttpContext context) { string requestPath = context.Request.Path.Value; // 一. 跨过校验 //1 默认启动页 if (requestPath.Equals("/")) { context.Response.StatusCode = 200; //请求成功 await context.Response.WriteAsync("Welcome To Ship_GateWay Index"); return; } //2 登录服务 //表示如果登录认证服务配置在网关下,则跳过网关中的各种校验 if (requestPath.StartsWith("/Ship_Auth/")) { await _next.Invoke(context); } //二. Token的基本校验(非空、准确性、是否过期) string myToken = context.Request.Headers["token"].FirstOrDefault(); if (string.IsNullOrEmpty(myToken)) { //返回值此处有两种写法 //写法1:状态码+返回string值 //context.Response.StatusCode = 401; //401未授权 //await context.Response.WriteAsync("token为空"); //写法2: Http状态码永远是200 + json值(可以在里面定义自己StatusCode) await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 401, Content = "非法请求,token为空" }); return; } //校验auth的正确性 var result = JWTHelp.JWTJieM(myToken, _configuration["JWTSecret"]); if (result == "expired") { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 401, Content = "非法请求,参数已经过期" }); return; } if (result == "invalid") { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 401, Content = "非法请求,未通过校验" }); return; } if (result == "error") { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 401, Content = "非法请求,解析错误" }); return; } //三. 冻结校验 JwtUserInfo userInfo = JsonHelp.ToObject<JwtUserInfo>(result); string userId = userInfo.userId; if (RedisHelper.SIsMember("frozenUserList", userId)) { //403无权限 await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "该用户已被冻结" }); return; } //四. 顶下线校验/手动让token失效校验 long jwtVersion = userInfo.userVesion; long factVesion = Convert.ToInt32(RedisHelper.HGet("userVersionList", userId)); if (jwtVersion < factVesion) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "您的登录已失效,请重新登录" }); //403无权限 return; } //五. 一级鉴权--微服务访问 string perKey = "user_perList_" + userId; List<string> AllPerList = RedisHelper.Get<List<string>>(perKey); // 开头结尾各一个斜杠,中间一个元素 List<string> onePerList = [.. AllPerList.Where(s => s.StartsWith('/') && s.EndsWith('/') && s.Split('/').Length == 3)]; if (requestPath.StartsWith("/Ship_BackEnd/")) { if (!onePerList.Contains("/Ship_BackEnd/")) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "该用户不具备访问Ship_BackEnd微服务的权限" }); //403无权限 return; } } if (requestPath.StartsWith("/Ship_CrewCompany/")) { if (!onePerList.Contains("/Ship_CrewCompany/")) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "该用户不具备访问Ship_CrewCompany微服务的权限" }); //403无权限 return; } } if (requestPath.StartsWith("/Ship_ManageCompany/")) { if (!onePerList.Contains("/Ship_ManageCompany/")) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "该用户不具备访问Ship_ManageCompany微服务的权限" }); //403无权限 return; } } if (requestPath.StartsWith("/Ship_Trade/")) { if (!onePerList.Contains("/Ship_Trade/")) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = "该用户不具备访问Ship_Trade微服务的权限" }); //403无权限 return; } } //六.二级鉴权--Action的访问 List<string> twoPerList = [.. AllPerList.Except(onePerList)]; if (!twoPerList.Contains(requestPath)) { await context.Response.WriteAsJsonAsync(new AuthData() { StatusCode = 403, Content = $"该用户不具备访问{requestPath}接口的权限" }); //403无权限 return; } //最后: 继续走后面的管道 await _next.Invoke(context); } }
4 后台微服务
[Route("Api/[controller]/[action]")]
public class AdminController : Controller
{
/// <summary>
/// 获取所有角色信息
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult GetAllRoles()
{
return Json(new { status = "ok", msg = "获取成功", data = new List<string>() { "admin","frozen"} });
}
}
5 船员公司微服务
[Route("Api/[controller]/[action]")]
public class ShipCrewController : Controller
{
/// <summary>
/// 获取船员信息
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult GetCrewList()
{
return Json(new { status = "ok", msg = "获取成功", data = new List<string>() { "张三", "李四" } });
}
}
三. 测试
1. 登录测试
通过网关 http://127.0.0.1:8100/Ship_Auth/Auth/CheckLogin 访问登录接口,返回token等信息,证明“Ship_Auth微服务”放在网关的后面, 但是可以跳过校验。
2.Token校验测试
携带Token访问Ship_CrewCompany下的接口,http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList, 依次缺失token、修改一下token值、过一段时间模拟过期,均无法通过校验。
3.冻结测试
(1).先访问冻结接口 http://127.0.0.1:8100/Ship_Auth/Auth/SetFrozenUser,将用户10001冻结
(2).然后访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList 接口,提示被冻结
(3).再访问问解冻接口 http://127.0.0.1:8100/Ship_Auth/Auth/UnFrozenUser,将用户10001解冻
(4).重新访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList 接口,正常使用
4. 顶下线测试
(1) 先访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList 接口正常使用
(2) 再访问登录接口 http://127.0.0.1:8100/Ship_Auth/Auth/CheckLogin
(3) 重新访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList ,发现登录已经失效了
5.Token手动失效测试
(1) 先访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList 接口正常使用
(2) 再访问手动失效接口 http://127.0.0.1:8100/Ship_Auth/Auth/SetNoEffectToken
(3) 重新访问 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList ,发现登录已经失效了
6. 一级鉴权--微服务层次测试
(1) 先访问登录接口,获取所有权限: http://127.0.0.1:8100/Ship_Auth/Auth/CheckLogin
(2) 手动从redis中删掉 /Ship_CrewCompany/ 一级权限
(3). 然后访问船员接口 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList,
提示:该用户不具备访问Ship_CrewCompany微服务的权限
7. 二级鉴权--Action层次测试
(1) 先访问登录接口,获取所有权限: http://127.0.0.1:8100/Ship_Auth/Auth/CheckLogin
(2) 手动从redis中删掉 /Ship_CrewCompany/ShipCrew/GetCrewList 二级权限
(3).然后访问船员接口 http://127.0.0.1:8100/Ship_CrewCompany/ShipCrew/GetCrewList,
提示:该用户不具备访问/Ship_CrewCompany/ShipCrew/GetCrewList接口的权限
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。