1 项目搭建
这里我们API网关采用webAPI+Ocelot构建,首先在解决方案下创建文件夹apigateway并添加空白API,不包含Controller和Swagger,项目命名为Demo.Gateway。添加引用:Ocelot和IdentityServer4.AccessTokenValidation。其中IdentityServer4.AccessTokenValidation用于身份认证。.
编辑项目配置文件appsettings.json,按服务规划,我们设置端口号为4000,添加配置项
"urls": "http://*:4000"
"AuthService": "http://localhost:4100"修改Program.cs文件改为如下内容:
using IdentityServer4.AccessTokenValidation;using Ocelot.DependencyInjection;using Ocelot.Middleware;;var builder = WebApplication.CreateBuilder(args);//加载配置文件ocelot.jsonbuilder.Configuration.AddJsonFile("ocelot.json");//配置身份认证服务Action<IdentityServerAuthenticationOptions> optionsAuth = o =>{o.Authority = builder.Configuration["AuthService"]; //身份认证服务地址o.SupportedTokens = SupportedTokens.Jwt; //Token接入方式o.ApiSecret = "secret";o.RequireHttpsMetadata = false;o.JwtValidationClockSkew = TimeSpan.FromSeconds(0); //为防止有效期出现误差};builder.Services.AddAuthentication().AddIdentityServerAuthentication("DemoAuth", optionsAuth);builder.Services.AddOcelot();var app = builder.Build();app.UseAuthentication();app.UseOcelot().Wait();app.Run();
2 路由规则配置
按照上一步的设置,我们需要将路由配置存放于Demo.Gateway项目ocelot.json文件中。
因为服务层服务不对外暴露访问接口,所以不需要对服务层服务配置网关路由,需要对聚合服务层服务做路由转发规则。同时,在身份认证服务中,我们只需要对外暴露登录、刷新Token相关接口,原属于OAuth相关的接口不需要对外暴露,所以不做路由规则匹配。
详细路由规则匹配规范请参考官方文档:https://ocelot.readthedocs.io/en/latest/features/configuration.html
ocelot.json配置如下:
{"Routes": [{"DownstreamPathTemplate": "/api/{url}","DownstreamScheme": "http","DownstreamHostAndPorts": [{"Host": "localhost","Port": 4100}],"UpstreamPathTemplate": "/ids/{url}","UpstreamHttpMethod": [ "Get","Post","Put","Delete" ]},{"DownstreamPathTemplate": "/api/{url}","DownstreamScheme": "http","DownstreamHostAndPorts": [{"Host": "localhost","Port": 6001}],"UpstreamPathTemplate": "/admin/{url}","UpstreamHttpMethod": [ "Get","Post","Put","Delete" ],"AuthenticationOptions": {"AuthenticationProviderKey": "DemoAuth","AllowedScopes": []}},{"DownstreamPathTemplate": "/api/{url}","DownstreamScheme": "http","DownstreamHostAndPorts": [{"Host": "localhost","Port": 6002}],"UpstreamPathTemplate": "/store/{url}","UpstreamHttpMethod": [ "Get","Post","Put","Delete" ],"AuthenticationOptions": {"AuthenticationProviderKey": "DemoAuth","AllowedScopes": []}}],"GlobalConfiguration": {"BaseUrl": "https://localhost:4000"}}
UpstreamPathTemplate和DownstreamPathTemplate分别为上游请求路由和下游服务路由,前者为对外暴露的路由地址,后者为实际服务提供者的路由地址,通过{url}做表达式匹配。例如我们身份认证服务
因为我们这里使用HTTP接口,且HTTPS证书不在网关上配置而是在Ingress中配置,所以DownstreamScheme值设置为http
UpstreamHttpMethod代表可允许的请求方式,如Get、Post、Put、Delete等,可依据实际需要进行配置
DownstreamHostAndPorts表示下游服务地址,可以设置多个,我们这里测试环境设置一个固定的即可。生产环境可使用Kubernetes的Service名称+端口号,具体会在后面文章中介绍。
AuthenticationOptions为身份配置选项,其中AuthenticationProviderKey必须和Program中 .AddIdentityServerAuthentication("DemoAuth", optionsAuth); 的第一个参数一致。这里身份认证服务登录和刷新Token接口不需要身份认证,所以不带此参数。
3 集成测试
启动API网关(Demo.Gateway)、身份认证服务(Demo.Identity.IdentityServer)、商城服务(Demo.Store.HttpApi.Host)、订单服务(Demo.OrderManager.HttpApi.Host),使用PostMan等工具访问获取订单列表接口,其网关地址为http://127.0.0.1:4000/store/app/store-order,方式为GET。此时会报401错误。
需要我们先通过用户名密码获取Token,使用ABP默认管理员账户admin,密码为:1q2w3E*(注意大小写)。以Post方式调用接口http://127.0.0.1:4000/ids/auth/login,Body以JSON格式写入以下内容:
{"UserName": "admin","Password": "1q2w3E*"}
登录成功返回示例如下:
{"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6……","refreshToken": "81871D2F7764A173277AFF3FD41……","expireInSeconds": 72000,"hasError": false,"message": ""}
其中accessToken为访问令牌,refreshToken为刷新令牌。
再次访问获取订单接口,添加Header:Key为Authorization,Value为Bearer+空格+访问令牌,返回状态码200并返回如下数据表示测试通过:
{"totalCount": 0,"items": []}
4 当前用户
按照我们之前的设计,只需要在API网关中做身份认证,下游服务不需要二次做身份认证,那么我们可以在网关中解析用户ID并添加至Header传向下游服务。
在Demo.Gateway项目中添加中间件如下:
using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Authentication.JwtBearer;namespace Demo.Gateway;public static class JwtTokenMiddleware{public static IApplicationBuilder UseJwtTokenMiddleware(this IApplicationBuilder app, string schema = JwtBearerDefaults.AuthenticationScheme){return app.Use(async (ctx, next) =>{//如果上游服务客户端传入UserID则先清空if (ctx.Request.Headers.ContainsKey("UserId")){ctx.Request.Headers.Remove("UserId");}if (ctx.User.Identity?.IsAuthenticated != true){var result = await ctx.AuthenticateAsync(schema);//如果如果可以获取到用户信息则将用户ID写入Headerif (result.Succeeded && result.Principal != null){ctx.User = result.Principal;var uid = result.Principal.Claims.First(x => x.Type == "sub").Value;ctx.Request.Headers.Add("UserId",uid);}}await next();});}}
app.UseJwtTokenMiddleware("DemoAuth");其参数和 .AddIdentityServerAuthentication("DemoAuth", optionsAuth); 第一个参数值一致。
这样我们就将网关解析的UserId包含在转发的Header中,下一步我们需要在下游服务中获取UserId并作为当前用户的ID使用。
using System.Security.Claims;using Microsoft.AspNetCore.Http;using Volo.Abp.Security.Claims;namespace Demo.Abp.Extension;public class CurrentUserMiddleware{private readonly RequestDelegate _next;private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;public CurrentUserMiddleware(RequestDelegate next,ICurrentPrincipalAccessor currentPrincipalAccessor){_next = next;_currentPrincipalAccessor = currentPrincipalAccessor;}public async Task InvokeAsync(HttpContext context){string uid = context.Request.Headers["UserId"];if (!uid.IsNullOrEmpty()){//如果获取到用户ID,则作为当前用户IDvar newPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]{new Claim(AbpClaimTypes.UserId, uid),}));using (_currentPrincipalAccessor.Change(newPrincipal)){await _next(context);}}else{//如果未获取到用户ID则忽略此项继续执行后面的逻辑await _next(context);}}}
using Microsoft.AspNetCore.Builder;namespace Demo.Abp.Extension;public static class CurrentUserExtensions{/// <summary>/// 注册当前用户中间件/// </summary>public static void UseCurrentUser(this IApplicationBuilder app){app.UseMiddleware<CurrentUserMiddleware>();}}
app.UseCurrentUser();在公共类库中添加AddHeaderHandler类如下:using Volo.Abp.DependencyInjection;using Volo.Abp.Users;
namespace Demo.Abp.Extension;public class AddHeaderHandler : DelegatingHandler, ITransientDependency{private ICurrentUser _currentUser;public AddHeaderHandler(ICurrentUser currentUser){_currentUser = currentUser;}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken){var headers =request.Headers;if (!headers.Contains("UserId")){headers.Add("UserId",_currentUser.Id?.ToString());}return await base.SendAsync(request, cancellationToken);}}
context.Services.AddTransient<AddHeaderHandler>();context.Services.AddHttpClient(ProductManagerRemoteServiceConsts.RemoteServiceName).AddHttpMessageHandler<AddHeaderHandler>();