ABP微服務系列學習-搭建自己的微服務結構(三)

2023-03-01 12:00:50

上一篇我們基礎服務初步搭建完畢,接下來我們整一下認證和閘道器。

搭建認證服務

認證服務的話,ABP CLI生成的所有模板都包括了一個AuthServer。我們直接生成模板然後微調一下就可以直接用了。

abp new FunShow -t app --tiered

使用命令建立模板後,我們可以找到一個AuthServer。把專案移動到Apps目錄下,然後我們開始改造一下這個專案。
首先修改專案檔案的參照設定
修改EFCore專案參照為AdministrationService.EntityFrameworkCore和IdentityService.EntityFrameworkCore,
然後新增Shared.Localization和Shared.Hosting.AspNetCore專案參照,別的基本不用怎麼修改,完整專案設定為:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <Import Project="..\..\..\..\common.props" />

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <UserSecretsId>b83bc18b-a6ca-4e2d-a827-26ffaff35dce</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <DockerfileContext>..\..\..\..</DockerfileContext>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="6.0.5" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Volo.Abp.Caching.StackExchangeRedis" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.EventBus.RabbitMQ" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.BackgroundJobs.RabbitMQ" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.Account.Web.OpenIddict" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.Account.Application" Version="7.0.0" />
    <PackageReference Include="Volo.Abp.Account.HttpApi" Version="7.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\..\..\services\administration\src\FunShow.AdministrationService.EntityFrameworkCore\FunShow.AdministrationService.EntityFrameworkCore.csproj" />
    <ProjectReference Include="..\..\..\..\services\identity\src\FunShow.IdentityService.EntityFrameworkCore\FunShow.IdentityService.EntityFrameworkCore.csproj" />
    <ProjectReference Include="..\..\..\..\shared\FunShow.Shared.Hosting.AspNetCore\FunShow.Shared.Hosting.AspNetCore.csproj" />
    <ProjectReference Include="..\..\..\..\shared\FunShow.Shared.Localization\FunShow.Shared.Localization.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite" Version="2.0.0-*" />
  </ItemGroup>

  <ItemGroup>
    <Compile Remove="Logs\**" />
    <Content Remove="Logs\**" />
    <EmbeddedResource Remove="Logs\**" />
    <None Remove="Logs\**" />
  </ItemGroup>

</Project>

然後修改Program檔案,主要是紀錄檔設定修改一下,別的不用改動

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using FunShow.Shared.Hosting.AspNetCore;
using Serilog;

namespace FunShow.AuthServer;

public class Program
{
    public async static Task<int> Main(string[] args)
    {
        var assemblyName = typeof(Program).Assembly.GetName().Name;

        SerilogConfigurationHelper.Configure(assemblyName);

        try
        {
            Log.Information($"Starting {assemblyName}.");
            var builder = WebApplication.CreateBuilder(args);
            builder.Host
                .AddAppSettingsSecretsJson()
                .UseAutofac()
                .UseSerilog();
            await builder.AddApplicationAsync<FunShowAuthServerModule>();
            var app = builder.Build();
            await app.InitializeApplicationAsync();
            await app.RunAsync();
            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, $"{assemblyName} terminated unexpectedly!");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

修改module.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using FunShow.AdministrationService.EntityFrameworkCore;
using FunShow.IdentityService.EntityFrameworkCore;
using FunShow.Shared.Hosting.AspNetCore;
using Prometheus;
using StackExchange.Redis;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.Auditing;
using Volo.Abp.BackgroundJobs.RabbitMQ;
using Volo.Abp.Caching;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Emailing;
using Volo.Abp.EventBus.RabbitMq;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict;
using Volo.Abp.UI.Navigation.Urls;
using Volo.Abp.VirtualFileSystem;
using Microsoft.AspNetCore.HttpOverrides;
using FunShow.Shared.Localization;

namespace FunShow.AuthServer;

[DependsOn(
    typeof(AbpCachingStackExchangeRedisModule),
    typeof(AbpEventBusRabbitMqModule),
    typeof(AbpBackgroundJobsRabbitMqModule),
    typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule),
    typeof(AbpAccountWebOpenIddictModule),
    typeof(AbpAccountApplicationModule),
    typeof(AbpAccountHttpApiModule),
    typeof(AdministrationServiceEntityFrameworkCoreModule),
    typeof(IdentityServiceEntityFrameworkCoreModule),
    typeof(FunShowSharedHostingAspNetCoreModule),
    typeof(FunShowSharedLocalizationModule)
)]
public class FunShowAuthServerModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        var hostingEnvironment = context.Services.GetHostingEnvironment();
        var configuration = context.Services.GetConfiguration();
    
    PreConfigure<OpenIddictBuilder>(builder =>
    {
    	builder.AddValidation(options =>
    {
    options.AddAudiences("AccountService");
    options.UseLocalServer();
    options.UseAspNetCore();
    });
    });
    if (!hostingEnvironment.IsDevelopment())
    {
    	PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
    	{
    	options.AddDevelopmentEncryptionAndSigningCertificate = false;
    	});
    
        PreConfigure<OpenIddictServerBuilder>(builder =>
        {
            builder.AddSigningCertificate(GetSigningCertificate(hostingEnvironment, configuration));
            builder.AddEncryptionCertificate(GetSigningCertificate(hostingEnvironment, configuration));
            });
        }
    }
    
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
    //You can disable this setting in production to avoid any potential security risks.
        Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
        
        var hostingEnvironment = context.Services.GetHostingEnvironment();
        var configuration = context.Services.GetConfiguration();
        
        ConfigureBundles();
        ConfigureSwagger(context, configuration);
        ConfigureSameSiteCookiePolicy(context);
        ConfigureExternalProviders(context);
        
        Configure<AbpMultiTenancyOptions>(options =>
        {
        	options.IsEnabled = true;
        });
        
        Configure<AbpAuditingOptions>(options =>
        {
            options.ApplicationName = "AuthServer";
        });
        
        Configure<AppUrlOptions>(options =>
        {
            options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
            options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
        });
        
        Configure<AbpDistributedCacheOptions>(options =>
        {
        	options.KeyPrefix = "FunShow:";
        });
        
        var dataProtectionBuilder = context.Services.AddDataProtection().SetApplicationName("FunShow");
        var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
        dataProtectionBuilder.PersistKeysToStackExchangeRedis(redis, "FunShow-Protection-Keys");
        
        context.Services.AddCors(options =>
        {
        	options.AddDefaultPolicy(builder =>
            {
            builder
            .WithOrigins(
            configuration["App:CorsOrigins"]
            .Split(",", StringSplitOptions.RemoveEmptyEntries)
            .Select(o => o.Trim().RemovePostFix("/"))
            .ToArray()
            )
            .WithAbpExposedHeaders()
            .SetIsOriginAllowedToAllowWildcardSubdomains()
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
            });
        });
        
        #if DEBUG
        context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
        #endif
        
        if (hostingEnvironment.IsDevelopment())
        {
            Configure<AbpVirtualFileSystemOptions>(options =>
            {
            options.FileSets.ReplaceEmbeddedByPhysical<FunShowSharedLocalizationModule>(Path.Combine(
            hostingEnvironment.ContentRootPath,
            $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}shared{Path.DirectorySeparatorChar}FunShow.Shared.Localization"));
            });
        }
    }
    
    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        
        var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
        
        if (env.IsDevelopment())
        {
        	app.UseDeveloperExceptionPage();
        }
    
        app.UseAbpRequestLocalization();
        
        if (!env.IsDevelopment())
        {
        	app.UseErrorPage();
        }
        var forwardOptions = new ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
            RequireHeaderSymmetry = false
        };
    
        forwardOptions.KnownNetworks.Clear();
        forwardOptions.KnownProxies.Clear();
        
    // ref: https://github.com/aspnet/Docs/issues/2384
        app.UseForwardedHeaders(forwardOptions);
        
        app.UseCorrelationId();
        app.UseAbpSecurityHeaders();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseCors();
        app.UseCookiePolicy();
        app.UseHttpMetrics();
        app.UseAuthentication();
        app.UseAbpOpenIddictValidation();
        app.UseAbpSerilogEnrichers();
        app.UseUnitOfWork();
        app.UseAuthorization();
        app.UseSwagger();
        app.UseAbpSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "Account Service API");
            options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
        });
        	app.UseAuditing();
        	app.UseConfiguredEndpoints(endpoints =>
            {
            	endpoints.MapMetrics();
            });
    }
    
    private void ConfigureBundles()
    {
        Configure<AbpBundlingOptions>(options =>
        {
            options.StyleBundles.Configure(
            LeptonXLiteThemeBundles.Styles.Global,
            bundle =>
            {
            	bundle.AddFiles("/global-styles.css");
            }
        );
        });
    }
    
    private void ConfigureExternalProviders(ServiceConfigurationContext context)
    {
    	context.Services.AddAuthentication();
    }
    
    private X509Certificate2 GetSigningCertificate(IWebHostEnvironment hostingEnv, IConfiguration configuration)
    {
        var fileName = "authserver.pfx";
        var passPhrase = "2D7AA457-5D33-48D6-936F-C48E5EF468ED";
        var file = Path.Combine(hostingEnv.ContentRootPath, fileName);
        
        if (!File.Exists(file))
        {
        	throw new FileNotFoundException($"Signing Certificate couldn't found: {file}");
        }
    
    	return new X509Certificate2(file, passPhrase);
    }
    
    private void ConfigureSwagger(ServiceConfigurationContext context, IConfiguration configuration)
    {
        SwaggerConfigurationHelper.ConfigureWithAuth(
        context: context,
        authority: configuration["AuthServer:Authority"],
        scopes: new Dictionary<string, string> {
        /* Requested scopes for authorization code request and descriptions for swagger UI only */
        { "AccountService", "Account Service API" }
        },
        apiTitle: "Account Service API"
        );
    }
    
    private void ConfigureSameSiteCookiePolicy(ServiceConfigurationContext context)
    {
    	context.Services.AddSameSiteCookiePolicy();
    }
}

最後修改組態檔

{
  "App": {
    "SelfUrl": "https://localhost:44322",
    "CorsOrigins": "http://localhost:4200,http://localhost:9527,https://localhost:44307,https://localhost:44325,https://localhost:44353,https://localhost:44367,https://localhost:44388,https://localhost:44381,https://localhost:44361",
    "RedirectAllowedUrls": "http://localhost:4200,https://localhost:44307,https://localhost:44321,http://localhost:9527"
  },
  "AuthServer": {
    "Authority": "https://localhost:44322",
    "RequireHttpsMetadata": "true",
    "SwaggerClientId": "WebGateway_Swagger"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "AdministrationService": "Host=localhost;Port=5432;User ID=postgres;password=myPassw0rd;Pooling=true;Database=FunShow_Administration;",
    "IdentityService": "Host=localhost;Port=5432;User ID=postgres;password=myPassw0rd;Pooling=true;Database=FunShow_Identity;"
  },
  "StringEncryption": {
    "DefaultPassPhrase": "fCrJICTG3WoyissG"
  },
  "Redis": {
    "Configuration": "localhost:6379"
  },
  "RabbitMQ": {
    "Connections": {
      "Default": {
        "HostName": "localhost"
      }
    },
    "EventBus": {
      "ClientName": "FunShow_AuthServer",
      "ExchangeName": "FunShow"
    }
  },
  "ElasticSearch": {
    "Url": "http://localhost:9200"
  }
}

這樣我們認證服務即修改完成。

搭建閘道器服務

閘道器服務我們直接新建一個空asp.net core專案。
然後只需要新增一個我們的Shared.Hosting.Gateways專案參照即可。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <Import Project="..\..\..\..\common.props" />

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\..\..\shared\FunShow.Shared.Hosting.Gateways\FunShow.Shared.Hosting.Gateways.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Compile Remove="Logs\**" />
    <Content Remove="Logs\**" />
    <EmbeddedResource Remove="Logs\**" />
    <None Remove="Logs\**" />
  </ItemGroup>

</Project>

修改Program.cs

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using FunShow.Shared.Hosting.AspNetCore;
using Serilog;

namespace FunShow.WebGateway;

public class Program
{
    public async static Task<int> Main(string[] args)
    {
        var assemblyName = typeof(Program).Assembly.GetName().Name;

        SerilogConfigurationHelper.Configure(assemblyName);

        try
        {
            Log.Information($"Starting {assemblyName}.");
            var builder = WebApplication.CreateBuilder(args);
            builder.Host
                .AddAppSettingsSecretsJson()
                .AddYarpJson()
                .UseAutofac()
                .UseSerilog();
            await builder.AddApplicationAsync<FunShowWebGatewayModule>();
            var app = builder.Build();
            await app.InitializeApplicationAsync();
            await app.RunAsync();
            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, $"{assemblyName} terminated unexpectedly!");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

這裡和認證服務基本一致,就是多了一個AddYarpJson()來新增我們的yarp的組態檔。
在目錄下新建yarp.json檔案,新增我們的yarp設定內容。設定叢集和路由如下:

{
  "ReverseProxy": {
    "Routes": {
      "Account Service": {
        "ClusterId": "accountCluster",
        "Match": {
          "Path": "/api/account/{**everything}"
        }
      },
      "Identity Service": {
        "ClusterId": "identityCluster",
        "Match": {
          "Path": "/api/identity/{**everything}"
        }
      },
      "Administration Service": {
        "ClusterId": "administrationCluster",
        "Match": {
          "Path": "/api/abp/{**everything}"
        }
      },
      "Logging Service": {
        "ClusterId": "loggingCluster",
        "Match": {
          "Path": "/api/LoggingService/{**everything}"
        }
      },
      "feature-management-route": {
        "ClusterId": "feature-management-cluster",
        "Match": {
          "Path": "/api/feature-management/{**everything}"
        }
      },
      "permission-management-route": {
        "ClusterId": "permission-management-cluster",
        "Match": {
          "Path": "/api/permission-management/{**everything}"
        }
      },
      "setting-management-route": {
        "ClusterId": "setting-management-cluster",
        "Match": {
          "Path": "/api/setting-management/{**everything}"
        }
      }
    },
    "Clusters": {
      "accountCluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44322"
          }
        }
      },
      "identityCluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44388"
          }
        }
      },
      "administrationCluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44367"
          }
        }
      },
      "loggingCluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:45124"
          }
        }
      },
      "feature-management-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44367"
          }
        }
      },
      "permission-management-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44367"
          }
        }
      },
      "setting-management-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44367"
          }
        }
      }
    }
  }
}

在appsettings.json檔案新增我們認證服務的地址

{
  "App": {
    "SelfUrl": "https://localhost:44325",
    "CorsOrigins": "http://localhost:4200,https://localhost:44307,http://localhost:9527"
  },
  "AuthServer": {
    "Authority": "https://localhost:44322",
    "RequireHttpsMetadata": "true",
    "SwaggerClientId": "WebGateway_Swagger"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Redis": {
    "Configuration": "localhost:6379"
  },
  "ElasticSearch": {
    "Url": "http://localhost:9200"
  }
}

最後我們新增FunShowWebGatewayModule檔案。設定我們yarp的服務。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using FunShow.Shared.Hosting.AspNetCore;
using FunShow.Shared.Hosting.Gateways;
using Volo.Abp;
using Volo.Abp.Modularity;

namespace FunShow.WebGateway;

[DependsOn(
    typeof(FunShowSharedHostingGatewaysModule)
)]
public class FunShowWebGatewayModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // Enable if you need hosting environment
        // var hostingEnvironment = context.Services.GetHostingEnvironment();
        var configuration = context.Services.GetConfiguration();
        var hostingEnvironment = context.Services.GetHostingEnvironment();
        
        SwaggerConfigurationHelper.ConfigureWithAuth(
            context: context,
            authority: configuration["AuthServer:Authority"],
            scopes: new
            Dictionary<string, string> /* Requested scopes for authorization code request and descriptions for swagger UI only */ {
            { "AccountService", "Account Service API" },
            { "IdentityService", "Identity Service API" },
            { "AdministrationService", "Administration Service API" },
            { "LoggingService", "Logging Service API" }
            },
            apiTitle: "Web Gateway API"
        );
        
        context.Services.AddCors(options =>
        {
        	options.AddDefaultPolicy(builder =>
        {
        builder
            .WithOrigins(
                configuration["App:CorsOrigins"]
                .Split(",", StringSplitOptions.RemoveEmptyEntries)
                .Select(o => o.Trim().RemovePostFix("/"))
                .ToArray()
            )
            .WithAbpExposedHeaders()
            .SetIsOriginAllowedToAllowWildcardSubdomains()
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
            });
        });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        
        if (env.IsDevelopment())
        {
        	app.UseDeveloperExceptionPage();
        }
        app.UseCorrelationId();
        app.UseAbpSerilogEnrichers();
        app.UseCors();
        app.UseSwaggerUIWithYarp(context);
        
        app.UseRewriter(new RewriteOptions()
            // Regex for "", "/" and "" (whitespace)
            .AddRedirect("^(|\\|\\s+)$", "/swagger"));
            
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
        	endpoints.MapReverseProxy();
        });
    }
}

UseSwaggerUIWithYarp是從我們Yarp組態檔中讀取服務資訊去構造swagger路由設定。
好了,到這我們認證服務和閘道器服務也搭建完畢,下一篇我們開始遷移資料庫。