Admin.NET开源版微服务改造记录
将Admin.NET.Core项目拆分成两个项目:Admin.NET.Common,Admin.NET.Core
Admin.NET.Common放基础工具类
Admin.NET.Core放框架核心类库
AspireApp.AppHost中的AppHost.cs配置:
using Aspire.Hosting;
using Aspire.Hosting.Dapr;
using AspireApp.AppHost;
var builder = DistributedApplication.CreateBuilder(args);
var postgresQL = builder.AddPostgres("postgresQL")
.WithImage("ankane/pgvector")
.WithImageTag("latest")
.WithLifetime(ContainerLifetime.Persistent)
.WithHealthCheck()
.WithPgWeb();
var postgres = postgresQL.AddDatabase("postgres");
var postgres2 = postgresQL.AddDatabase("postgres2");
//var redis = builder.AddRedis("redis").WithLifetime(ContainerLifetime.Persistent)
// .WithHealthCheck()
// .WithRedisCommander();
// 使用 RabbitMQ 作为 Pub/Sub 组件
//var rabbitmq = builder.AddRabbitMQ("rabbitmq")
// .WithLifetime(ContainerLifetime.Persistent)
// .WithHealthCheck()
// .WithManagementPlugin();
// 1. 定义共享目录的绝对路径(建议指向 admin-net-core 的实际目录或独立的 shared 目录)
var uploadPath = Path.GetFullPath("../Admin.NET/Admin.NET.Core/wwwroot/upload");
// 确保目录存在
if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
// Admin.NET.Core 使用 Furion 的 Knife4j UI,不使用 Aspire 的 Swagger UI
var core = builder.AddProject<Projects.Admin_NET_Core>("admin-net-core")
.WithReference(postgres)
.WaitFor(postgres)
.WithSwaggerUI();
var baseApi = builder.AddProject<Projects.Base>("base")
.WithReference(postgres2)
.WithSwaggerUI()
.WithReference(core)
.WaitFor(postgres2)
.WaitFor(core);
var yarp = builder.AddProject<Projects.AspireApp_Yarp>("aspireapp-yarp")
.WithEndpoint( port: 5008, scheme: "http", name: "yarp-http") // 指定唯一名称
.WithEndpoint( port: 7008, scheme: "https", name: "yarp-https") // 指定唯一名称
.WithReference(core)
.WithReference(baseApi)
.WaitFor(core)
.WaitFor(baseApi);
//var frontend = builder.AddNodeApp(
// "frontend",
// scriptPath: "scripts/run-pnpm.cjs",
// workingDirectory: "../Web",
// args: new[] { "dev" }
// )
// .WithReference(yarp)
// .WaitFor(yarp);
builder.Build().Run();
AIP网关:AspireApp.Yarp
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddServiceDiscoveryDestinationResolver();
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapReverseProxy();
app.MapGet("/", () => "Hello World!");
app.Run();
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"core": {
"ClusterId": "core",
"Match": {
"Path": "/core/{**remainder}"
},
"Transforms": [
{ "PathRemovePrefix": "/core" },
{ "PathPrefix": "/" },
{ "RequestHeaderOriginalHost": "true" }
]
},
"base": {
"ClusterId": "base",
"Match": {
"Path": "/base/{**remainder}"
},
"Transforms": [
{ "PathRemovePrefix": "/base" },
{ "PathPrefix": "/" },
{ "RequestHeaderOriginalHost": "true" }
]
}
},
"Clusters": {
"core": {
"Destinations": {
"base_destination": {
"Address": "http+https://admin-net-core"
}
}
},
"base": {
"Destinations": {
"base_destination": {
"Address": "http+https://base"
}
}
}
}
}
}
业务项目(base项目)调用核心项目(core项目)的方法
我们使用Refit来服务调用
在base项目中的Startup.cs中
// 注册 JwtTokenHandler - 用于自动添加 JWT Token 到 Refit 请求
services.AddTransient<JwtTokenHandler>();
// 配置 Refit 客户端并添加 JWT Token 处理器
//系统配置
services.AddRefitClient<IConfigService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new("https+http://admin-net-core"))
.AddHttpMessageHandler<JwtTokenHandler>();
//系统菜单
services.AddRefitClient<IMenuService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new("https+http://admin-net-core"))
.AddHttpMessageHandler<JwtTokenHandler>();
//组织机构
services.AddRefitClient<IOrgService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new("https+http://admin-net-core"))
.AddHttpMessageHandler<JwtTokenHandler>();
//文件
services.AddRefitClient<IFileService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new("https+http://admin-net-core"))
.AddHttpMessageHandler<JwtTokenHandler>();
services.AddRefitClient<ITest>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new("https+http://apiservice"))
.AddHttpMessageHandler<JwtTokenHandler>();
其中最重要的权限接口调用
using Refit;
using GetAttribute = Refit.GetAttribute;
namespace Base.Rest;
public interface IMenuService
{
[Get("/api/sysMenu/getOwnBtnPermList")]
Task<List<string>> GetOwnBtnPermList();
[Get("/api/sysMenu/getAllBtnPermList")]
Task<List<string>> GetAllBtnPermList();
}
修改base项目的权限验证方法JwtHandler.cs
IConfigService IMenuService 就是我们定义的Refit接口,这样,每次权限验证就会调用core项目的API接口
using Admin.NET.Core;
using Admin.NET.Core.Service;
using Base.Rest;
using Furion;
using Furion.Authorization;
using Furion.DataEncryption;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;
namespace Admin.NET.Application;
public class JwtHandler : AppAuthorizeHandler
{
private readonly SysCacheService _sysCacheService = App.GetRequiredService<SysCacheService>();
private readonly IConfigService _sysConfigService = App.GetRequiredService<IConfigService>();
private static readonly IMenuService SysMenuService = App.GetRequiredService<IMenuService>();
/// <summary>
/// 自动刷新Token
/// </summary>
/// <param name="context"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
{
// 若当前账号存在黑名单中则授权失败
if (_sysCacheService.ExistKey($"{CacheConst.KeyBlacklist}{context.User.FindFirst(ClaimConst.UserId)?.Value}"))
{
context.Fail();
context.GetCurrentHttpContext().SignoutToSwagger();
return;
}
var tokenExpire = await _sysConfigService.GetTokenExpire();
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(), tokenExpire.Result, refreshTokenExpire.Result))
{
await AuthorizeHandleAsync(context);
}
else
{
context.Fail(); // 授权失败
var currentHttpContext = context.GetCurrentHttpContext();
if (currentHttpContext == null) return;
// 跳过由于 SignatureAuthentication 引发的失败
if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey)) return;
currentHttpContext.SignoutToSwagger();
}
}
public override async Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
{
// 已自动验证 Jwt Token 有效性
return await CheckAuthorizeAsync(httpContext);
}
/// <summary>
/// 权限校验核心逻辑
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
private static async Task<bool> CheckAuthorizeAsync(DefaultHttpContext httpContext)
{
// 登录模式判断PC、APP
if (App.User.FindFirst(ClaimConst.LoginMode)?.Value == ((int)LoginModeEnum.APP).ToString())
return true;
// 排除超管
if (App.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString())
return true;
// 路由名称
var routeName = httpContext.Request.Path.StartsWithSegments("/api")
? httpContext.Request.Path.Value![5..].Replace("/", ":")
: httpContext.Request.Path.Value![1..].Replace("/", ":");
// 获取用户拥有按钮权限集合
var ownBtnPermList = await SysMenuService.GetOwnBtnPermList();
if (ownBtnPermList.Exists(u => routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase)))
return true;
// 获取系统所有按钮权限集合
var allBtnPermList = await SysMenuService.GetAllBtnPermList();
return allBtnPermList.TrueForAll(u => !routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase));
}
}