• ABP vNext微服务架构详细教程——API网关


    1. 项目搭建

    这里我们API网关采用webAPI+Ocelot构建,首先在解决方案下创建文件夹apigateway并添加空白API,不包含Controller和Swagger,项目命名为Demo.Gateway。添加引用:Ocelot和IdentityServer4.AccessTokenValidation。其中IdentityServer4.AccessTokenValidation用于身份认证。

    编辑项目配置文件appsettings.json,按服务规划,我们设置端口号为4000,添加配置项

    "urls": "http://*:4000"

    在appsettings.json中添加以下配置用于配置身份认证服务的地址:

    "AuthService": "http://localhost:4100"

    在项目中添加一个新的配置文件ocelot.json,用于存放Ocelot的路由配置,其内容在后面步骤中添加

    修改Program.cs文件改为如下内容:

    using IdentityServer4.AccessTokenValidation;
    using Ocelot.DependencyInjection;
    using Ocelot.Middleware;;
    
    var builder = WebApplication.CreateBuilder(args);
    //加载配置文件ocelot.json
    builder.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();

     启动Demo.Gateway项目可正常运行,即基础代码搭建成功。

    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"
      }
    }

    这里Routes下每个对象为一个路由配置:

    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写入Header
                    if (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();
            });
        }
    }

    在Demo.Gateway项目中的Program.cs文件中 app.UseAuthentication(); 后加入以下代码:

    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,则作为当前用户ID
                var 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>();
        }
    }

    在聚合服务层服务中,我们可以直接使用此中间件来截取用户信息,需要在聚合服务层服务的HttpApi.Host项目的Module类中OnApplicationInitialization方法 if (env.IsDevelopment()) { app.UseExceptionHandler("/Error"); } 代码后添加如下代码:

    app.UseCurrentUser();

    由于我们使用的动态客户端代理访问服务层服务,无法直接在调用的Header的代码里传值,我们可以采用下面方法:

    在公共类库中添加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);
        }
    }

    然后我们在每个服务层服务的HttpApi.Client项目Module类的ConfigureServices方法开头位置添加如下代码:

    context.Services.AddTransient<AddHeaderHandler>();
    context.Services.AddHttpClient(ProductManagerRemoteServiceConsts.RemoteServiceName)
      .AddHttpMessageHandler<AddHeaderHandler>();

    之后我们在服务层服务的HttpApi.Host项目的Module类中OnApplicationInitialization方法中 app.UseAuthentication(); 前加入 app.UseCurrentUser(); 即可将用户ID传入服务层。

  • 相关阅读:
    数据库 连接(join)
    Linux top
    Game2048
    黑豆白豆问题
    1000个苹果10箱
    Jconsole
    八数码 Java实现
    两数之和
    磁盘调度算法
    常见应用网络层次
  • 原文地址:https://www.cnblogs.com/zklight/p/15834493.html
Copyright © 2020-2023  润新知