第七节:基于Ocelot网关层的微服务校验(手写jwt校验中间件和利用IdentityModel.Tokens.Jwt校验)
一. Ocelot+jwt 方案1
本节架构图:
1. 方案背景
截至目前,我们已经完成了可以通过Ocelot转发请求给业务服务器了,但现在还有一项工作没有做,那就是身份校验,当然我们可以直接写在业务服务器上,但是业务服务器会非常多,不利于维护,所以最佳的写法是写在Ocelot网关上,让Ocelot进行校验,校验通过了,才进行转发给业务服务器,并且业务服务器可能是在内网,外部的客户端无法直接访问。
2. 涉及到的项目
GateWay1、RequestTokenServer1、GoodsService、OrderService
用到的程序集:【Ocelot 16.0.1】【Ocelot.Provider.Consul 16.0.1】【JWT 7.2.1】
3. 设计思路
(1). RequestTokenServer1是认证服务器,提供GetAccessToken接口,客户端通过账号和密码访问,验证通过后返回授权码token。
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class AuthController : ControllerBase { public IConfiguration Configuration; public AuthController(IConfiguration configuration) { Configuration = configuration; } /// <summary> /// 获取请求Token /// </summary> /// <param name="account"></param> /// <param name="pwd"></param> /// <returns></returns> [HttpGet] public string GetAccessToken(string account, string pwd) { if (account != "admin" && pwd != "123456") { return JsonHelp.ToJsonString(new { status = "error", msg = "获取失败", token = "" }); } string secretKey = Configuration["SecretKey"]; //1.加密 //1.1 额外的header参数也可以不设置 var extraHeaders = new Dictionary<string, object> { {"myName", "ypf" }, }; //过期时间(可以不设置,下面表示签名后 20分钟过期) double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds; //进行组装 var payload = new Dictionary<string, object> { {"userId", "00000000001" }, {"userAccount", account }, {"exp",exp } }; //1.2 进行JWT签名 var myToken = JWTHelp.JWTJiaM(payload, secretKey, extraHeaders); return JsonHelp.ToJsonString(new { status = "ok", msg = "获取成功", token = myToken }); }
(2). GoodsService和OrderService分别以7001 和 7004端口启动,注册到Consul中。
(3). 新建GateWay1为网关服务器,这里主要使用路由功能进行演示,用于将客户端的请求转发给 GoodsService和OrderService 。
配置文件如下:
//模式三:将Ocelot与consul结合处理,在consul中已经注册业务服务器地址,在Ocelot端不需要再注册了(推荐用法) { "Routes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "ServiceName": "GoodsService", //Consul中的服务名称 "LoadBalancerOptions": { "Type": "RoundRobin" //轮询算法:依次调用在consul中注册的服务器 }, "UseServiceDiscovery": true, //启用服务发现(可以省略,因为会默认赋值) "UpstreamPathTemplate": "/GoodsService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ] }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "ServiceName": "OrderService", "LoadBalancerOptions": { "Type": "LeastConnection" //最小连接数算法 }, "UseServiceDiscovery": true, "UpstreamPathTemplate": "/OrderService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ] } ], //下面是配置Consul的地址和端口 "GlobalConfiguration": { //对应Consul的ip和Port(可以省略,因为会默认赋值) "ServiceDiscoveryProvider": { "Host": "127.0.0.1", "Port": 8500 } } }
(4). 核心:在GateWay1的Configure管道中 自定义jwt校验中间件,eg: app.UseMiddleware<JwtSafeMiddleware>(); 该中间件中拦截Get和Post请求,获取Header中的token,然后进行jwt校验,校验通过,则await _next.Invoke(context);继续走后面的中间件;校验不通过则 返回401未授权。
代码如下:
public class JwtSafeMiddleware { private readonly RequestDelegate _next; public IConfiguration _configuration; public JwtSafeMiddleware(RequestDelegate next, IConfiguration configuration) { _next = next; _configuration = configuration; } public async Task Invoke(HttpContext context) { //表示如果RequestTokenServer1配置在网关下,则访问它获取token的请求不走jwt校验哦 //if(!context.Request.Path.Value.StartsWith("/auth")) if (context.Request.Method == "GET" || context.Request.Method == "POST") { string myToken = context.Request.Headers["token"].FirstOrDefault(); if (string.IsNullOrEmpty(myToken)) { context.Response.StatusCode = 401; //401未授权 await context.Response.WriteAsync("token为空"); return; } //校验auth的正确性 var result = JWTHelp.JWTJieM(myToken, _configuration["SecretKey"]); if (result == "expired") { context.Response.StatusCode = 401; //401未授权 await context.Response.WriteAsync("非法请求,参数已经过期"); return; } else if (result == "invalid") { context.Response.StatusCode = 401; //401未授权 await context.Response.WriteAsync("非法请求,未通过校验"); return; } else if (result == "error") { context.Response.StatusCode = 401; //401未授权 await context.Response.WriteAsync("非法请求,未通过校验"); return; } else { //表示校验通过 } } await _next.Invoke(context); }
}
PS:
这里RequestTokenServer1认证服务器没有配置在GateWay1网关下,客户端可以直接访问该认证服务器。 当然,配置在网关下也行(eg: /auth/{url}代表认证server),那么只是需要在JwtSafeMiddleware中间件中加一层判断,比如if(!context.Request.Path.Value.StartsWith("/auth")) 即可。
4. 用PostMan充当客户端进行测试
(1).通过【consul.exe agent -dev】启动Consul。
(2).分别启动业务服务器:【dotnet GoodsService.dll --urls="http://*:7001" --ip="127.0.0.1" --port=7001 】
【dotnet OrderService.dll --urls="http://*:7004" --ip="127.0.0.1" --port=7004】
启动网关服务器:【dotnet GateWay1.dll --urls="http://*:7030" --ip="127.0.0.1" --port=7030】
启动认证服务器:【dotnet RequestTokenServer1.dll --urls="http://*:7031" --ip="127.0.0.1" --port=7031】
(3).测试1:
用PostMan以Get请求访问:http://127.0.0.1:7030/GoodsService/Catalog/GetMsg 报401,错误内容为“token为空”
用PostMan以Post且表单提交的方式访问:http://127.0.0.1:7030/OrderService/Buy/pOrder1 报401,错误内容为“token为空”。
(4). 测试2:
用PostMan以Get请求访问:http://127.0.0.1:7031/api/Auth/GetAccessToken?account=admin&pwd=123456 获取返回值中的token,然后把该token值放到header中,即token =“xxxx”, 然后再
用PostMan以Get请求访问:http://127.0.0.1:7030/GoodsService/Catalog/GetMsg 访问成功
用PostMan以Post且表单提交的方式访问:http://127.0.0.1:7030/OrderService/Buy/pOrder1 访问成功
二. Ocelot+jwt 方案2
利用【System.IdentityModel.Tokens.Jwt】程序集写的那套,由于需要加[Authorize],这个验证只能写在各个业务服务器上, 不是很好,此处不再重复了,如果需要,参照:https://www.cnblogs.com/yaopengfei/p/12162507.html
后续将介绍 Ocelot+ID4 做授权校验
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。