第五节:基于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" // 全局服务发现类型
    //}
  }
}
View Code

 

(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);



    }
}
View Code

 

 

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2025-05-29 14:27  Yaopengfei  阅读(24)  评论(1)    收藏  举报