From 413004fb5797c56dae2b2a5036429ac0e4e2136a Mon Sep 17 00:00:00 2001 From: token <239573049@qq.com> Date: Fri, 19 Jan 2024 02:49:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=B7=E6=B1=82=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Gateway/Domain/Users/Aggregations/User.cs | 32 - src/Gateway/Dto/RequestLogPanel.cs | 8 + src/Gateway/Entities/RequestLog.cs | 53 ++ src/Gateway/Gateway.csproj | 5 +- src/Gateway/GatewayModule.cs | 13 - src/Gateway/Options/RequestOptions.cs | 14 + src/Gateway/Program.cs | 68 ++- src/Gateway/Services/AuthorityService.cs | 49 -- src/Gateway/Services/RequestLogService.cs | 76 +++ src/Gateway/TypeHandlers/StringJsonHandler.cs | 17 + src/Gateway/appsettings.json | 38 +- web/src/App.tsx | 15 + web/src/layout/main-layout.tsx | 37 +- web/src/pages/gateway/index.tsx | 51 ++ web/src/pages/logger/index.tsx | 5 + web/src/pages/settings/index.tsx | 5 + web/yarn.lock | 546 ++++++------------ 17 files changed, 526 insertions(+), 506 deletions(-) delete mode 100644 src/Gateway/Domain/Users/Aggregations/User.cs create mode 100644 src/Gateway/Dto/RequestLogPanel.cs create mode 100644 src/Gateway/Entities/RequestLog.cs delete mode 100644 src/Gateway/GatewayModule.cs create mode 100644 src/Gateway/Options/RequestOptions.cs delete mode 100644 src/Gateway/Services/AuthorityService.cs create mode 100644 src/Gateway/Services/RequestLogService.cs create mode 100644 src/Gateway/TypeHandlers/StringJsonHandler.cs create mode 100644 web/src/pages/gateway/index.tsx create mode 100644 web/src/pages/logger/index.tsx create mode 100644 web/src/pages/settings/index.tsx diff --git a/src/Gateway/Domain/Users/Aggregations/User.cs b/src/Gateway/Domain/Users/Aggregations/User.cs deleted file mode 100644 index 4d6f245..0000000 --- a/src/Gateway/Domain/Users/Aggregations/User.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Gateway.Domain.Users.Aggregations; - -public class User : IdentityUser -{ - protected User() - { - - } - - public User(string userName,string email) : base(userName) - { - Email = email; - } - - public void SetPassword(string password) - { - PasswordHash = new PasswordHasher().HashPassword(this, password); - } - - public void SetPhoneNumber(string phoneNumber) - { - PhoneNumber = phoneNumber; - } - - public void Lockout() - { - LockoutEnabled = true; - LockoutEnd = DateTimeOffset.MaxValue; - } -} \ No newline at end of file diff --git a/src/Gateway/Dto/RequestLogPanel.cs b/src/Gateway/Dto/RequestLogPanel.cs new file mode 100644 index 0000000..3b11551 --- /dev/null +++ b/src/Gateway/Dto/RequestLogPanel.cs @@ -0,0 +1,8 @@ +namespace Gateway.Dto; + +public class RequestLogPanel +{ + public int RequestCount { get; set; } + + public DateTime StartTime { get; set; } +} \ No newline at end of file diff --git a/src/Gateway/Entities/RequestLog.cs b/src/Gateway/Entities/RequestLog.cs new file mode 100644 index 0000000..645617d --- /dev/null +++ b/src/Gateway/Entities/RequestLog.cs @@ -0,0 +1,53 @@ +using FreeSql.DataAnnotations; + +namespace Gateway.Entities; + +[Index("uk_path", nameof(Path), false)] +[Index("uk_ip", nameof(Ip), false)] +public class RequestLog +{ + [Column(IsIdentity = true, IsPrimary = true)] + public int Id { get; set; } + + public string Path { get; set; } + + public string Method { get; set; } + + public string QueryString { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 请求IP + /// + public string? Ip { get; set; } + + /// + /// 目标地址 + /// + public string? Target { get; set; } + + /// + /// 状态码 + /// + public int StatusCode { get; set; } + + /// + /// 执行时长(ms) + /// + public double ExecutionDuration { get; set; } + + /// + /// 浏览器信息 + /// + public string BrowserInfo { get; set; } + + /// + /// 扩展属性 + /// + [Column(MapType = typeof(string), StringLength = -1)] + public Dictionary ExtraProperties { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Gateway/Gateway.csproj b/src/Gateway/Gateway.csproj index 7339c14..c9b26b9 100644 --- a/src/Gateway/Gateway.csproj +++ b/src/Gateway/Gateway.csproj @@ -8,8 +8,9 @@ - - + + + diff --git a/src/Gateway/GatewayModule.cs b/src/Gateway/GatewayModule.cs deleted file mode 100644 index 7d71182..0000000 --- a/src/Gateway/GatewayModule.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CoreFlex.Jwt; -using CoreFlex.Module; - -namespace Gateway; - -[DependOns(typeof(CoreFlexJwtModule))] -public class GatewayModule : CoreFlexModule -{ - public override void ConfigureServices(CoreFlexServiceContext context) - { - - } -} \ No newline at end of file diff --git a/src/Gateway/Options/RequestOptions.cs b/src/Gateway/Options/RequestOptions.cs new file mode 100644 index 0000000..5b9f9db --- /dev/null +++ b/src/Gateway/Options/RequestOptions.cs @@ -0,0 +1,14 @@ +namespace Gateway.Options; + +public class RequestOptions +{ + /// + /// 过滤后缀 + /// + public static string[] FilterSuffixes { get; set; } = Array.Empty(); + + /// + /// 需要过滤的ContentType + /// + public static string[] FilterContentTypes { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Gateway/Program.cs b/src/Gateway/Program.cs index 63e3e30..5a7159a 100644 --- a/src/Gateway/Program.cs +++ b/src/Gateway/Program.cs @@ -1,31 +1,67 @@ -using CoreFlex.Jwt; -using CoreFlex.Module.Extensions; -using Gateway; +using System.Diagnostics; +using FreeSql; +using FreeSql.Internal; +using Gateway.Entities; +using Gateway.Options; using Gateway.Services; -using LiteDB; +using Gateway.TypeHandlers; + +Utils.TypeHandlers.TryAdd(typeof(Dictionary), new StringJsonHandler()); var builder = WebApplication.CreateBuilder(args); -builder.Configuration.GetSection(nameof(JwtOptions)).Get(); +builder.Configuration.GetSection(nameof(RequestOptions)).Get(); -await builder.AddCoreFlexAutoInjectAsync(); - -builder.Services.AddSingleton(_ => +builder.Services.AddSingleton(); +builder.Services.AddSingleton((_ => { - var dbPath = Path.Combine(builder.Environment.ContentRootPath, "gateway.db"); - return new LiteDatabase(dbPath); -}); + var freeSql = new FreeSqlBuilder() + .UseConnectionString(DataType.Sqlite, builder.Configuration.GetConnectionString("DefaultConnection")) + .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}")) //监听SQL语句 + .UseAutoSyncStructure(true) //自动同步实体结构到数据库,FreeSql不会扫描程序集,只有CRUD时才会生成表。 + .Build(); + return freeSql; +})); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); +app.Use((async (context, next) => +{ + if (RequestOptions.FilterSuffixes.Any(x => context.Request.Path.Value?.EndsWith(x) == true)) + return; + + var requestLogService = context.RequestServices.GetRequiredService(); + var ip = context.Connection.RemoteIpAddress?.ToString(); + + var requestLog = new RequestLog + { + Path = context.Request.Path, + Method = context.Request.Method, + QueryString = context.Request.QueryString.ToString(), + CreatedTime = DateTime.Now, + Ip = ip, + BrowserInfo = context.Request.Headers.UserAgent.ToString(), + }; + context.Items["RequestLog"] = requestLog; + var stopwatch = Stopwatch.StartNew(); + await next(); + stopwatch.Stop(); + + // 过滤Content-Type + if (RequestOptions.FilterContentTypes.Any(x => context.Response.ContentType?.Contains(x) == true)) + return; + + requestLog.ExecutionDuration = stopwatch.Elapsed.TotalMilliseconds; + requestLog.StatusCode = context.Response.StatusCode; + await requestLogService.LogAsync(requestLog); +})); + +app.MapGet("/api/panel", async (RequestLogService requestLogService, int hours) => + await requestLogService.PanelAsync(hours)); + app.MapReverseProxy(); -app.MapPost("/api/authority/token", async (AuthorityService service, string userName, string password) => - await service.GetTokenAsync(userName, password)); - -await app.Services.UseCoreFlexAsync(); - await app.RunAsync(); \ No newline at end of file diff --git a/src/Gateway/Services/AuthorityService.cs b/src/Gateway/Services/AuthorityService.cs deleted file mode 100644 index f671961..0000000 --- a/src/Gateway/Services/AuthorityService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Security.Claims; -using CoreFlex.Jwt; -using CoreFlex.Module; -using Gateway.Domain.Users.Aggregations; -using LiteDB; -using Microsoft.AspNetCore.Identity; - -namespace Gateway.Services; - -public class AuthorityService(ILiteDatabase database) : IScopedDependency -{ - public async Task GetTokenAsync(string userName, string password) - { - var userCollection = database.GetCollection(); - - var user = userCollection.FindOne(x => x.UserName == userName); - - if (user == null) - { - throw new Exception("用户不存在"); - } - - var passwordHasher = new PasswordHasher(); - - var verifyResult = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); - - if (verifyResult == PasswordVerificationResult.Failed) - { - throw new Exception("密码错误"); - } - - if (verifyResult == PasswordVerificationResult.SuccessRehashNeeded) - { - user.SetPassword(password); - - userCollection.Update(user); - } - - var doc = new Dictionary(); - // 使用微软提供的角色常量 - doc.Add(ClaimTypes.Role, "admin"); - doc.Add(ClaimTypes.NameIdentifier, user!.Id.ToString()); - doc.Add(ClaimTypes.Name, user.UserName); - doc.Add(ClaimTypes.Email, user.Email); - var token = JwtHelper.GeneratorAccessToken(doc); - - return token; - } -} \ No newline at end of file diff --git a/src/Gateway/Services/RequestLogService.cs b/src/Gateway/Services/RequestLogService.cs new file mode 100644 index 0000000..b29cc34 --- /dev/null +++ b/src/Gateway/Services/RequestLogService.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Threading.Channels; +using Gateway.Dto; +using Gateway.Entities; + +namespace Gateway.Services; + +/// +/// 请求日志服务处理 +/// +public class RequestLogService +{ + private readonly Channel _channel; + private readonly IFreeSql _freeSql; + + public RequestLogService(IFreeSql freeSql) + { + _freeSql = freeSql; + _channel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false, + }); + + Task.Run(HandleAsync); + } + + private async Task HandleAsync() + { + while (await _channel.Reader.WaitToReadAsync()) + { + while (_channel.Reader.TryRead(out var requestLog)) + { + await _freeSql.Insert(requestLog).ExecuteAffrowsAsync(); + } + } + } + + public async ValueTask LogAsync(RequestLog requestLog) + { + await _channel.Writer.WriteAsync(requestLog); + } + + public async Task> PanelAsync(int hours) + { + // hours是传递的多少小时,查询每小时的请求量,返回一个数组 + var result = await _freeSql.Ado.QueryAsync( + $""" + WITH RECURSIVE HoursBefore AS ( + SELECT strftime('%Y-%m-%d %H:00:00', 'now', 'localtime') AS StartTime, + strftime('%Y-%m-%d %H:59:59', 'now', 'localtime') AS EndTime + UNION ALL + SELECT datetime(StartTime, '-1 hour') AS StartTime, + datetime(EndTime, '-1 hour') AS EndTime + FROM HoursBefore + WHERE StartTime > datetime('now', 'localtime', '-' || {hours} || ' hours') + ) + SELECT + HB.StartTime, + COUNT(Log.Id) AS RequestCount + FROM + HoursBefore HB + LEFT JOIN + (SELECT * FROM RequestLog WHERE CreatedTime BETWEEN datetime('now', 'localtime', '-' || {hours} || ' hours') AND datetime('now', 'localtime')) AS Log + ON + Log.CreatedTime >= HB.StartTime + AND Log.CreatedTime <= HB.EndTime + GROUP BY + HB.StartTime + ORDER BY + HB.StartTime DESC + """); + + return result; + } +} \ No newline at end of file diff --git a/src/Gateway/TypeHandlers/StringJsonHandler.cs b/src/Gateway/TypeHandlers/StringJsonHandler.cs new file mode 100644 index 0000000..9bcf670 --- /dev/null +++ b/src/Gateway/TypeHandlers/StringJsonHandler.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using FreeSql.Internal.Model; + +namespace Gateway.TypeHandlers; + +public class StringJsonHandler : TypeHandler> +{ + public override Dictionary Deserialize(object value) + { + return JsonSerializer.Deserialize>((string)value); + } + + public override object Serialize(Dictionary value) + { + return JsonSerializer.Serialize(value); + } +} \ No newline at end of file diff --git a/src/Gateway/appsettings.json b/src/Gateway/appsettings.json index c6230a7..798a0ef 100644 --- a/src/Gateway/appsettings.json +++ b/src/Gateway/appsettings.json @@ -7,11 +7,39 @@ } }, "AllowedHosts": "*", - "JwtOptions": { - "Secret": "123456789asdfghjkl1&345678*asdfghjkl", - "EffectiveHours": 12, - "Issuer": "Gateway", - "Audience": "gateway" + "ConnectionStrings": { + "DefaultConnection": "Data Source=./Gateway.db" + }, + "RequestOptions": { + "FilterSuffixes": [ + ".js", + ".css", + ".jpg", + ".png", + ".gif", + ".ico", + ".svg", + ".woff", + ".woff2", + ".ttf", + ".js.map", + ".css.map" + ], + "FilterContentTypes": [ + "text/html", + "text/css", + "text/javascript", + "application/javascript", + "application/json", + "image/jpeg", + "image/png", + "image/gif", + "image/x-icon", + "image/svg+xml", + "font/woff", + "font/woff2", + "font/ttf" + ] }, "ReverseProxy": { "Routes": { diff --git a/web/src/App.tsx b/web/src/App.tsx index 10b2094..e27e047 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4,6 +4,9 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import MainLayout from './layout/main-layout'; import Home from './pages/home'; import 'reset-css'; +import Gateway from './pages/gateway'; +import Logger from './pages/logger'; +import Setting from './pages/settings'; const router = createBrowserRouter([{ path: "/", @@ -12,6 +15,18 @@ const router = createBrowserRouter([{ { path: "", element: + }, + { + path: "Gateway", + element: + }, + { + path: "Logger", + element: + }, + { + path: "Setting", + element: } ] }]); diff --git a/web/src/layout/main-layout.tsx b/web/src/layout/main-layout.tsx index aafee89..e362657 100644 --- a/web/src/layout/main-layout.tsx +++ b/web/src/layout/main-layout.tsx @@ -1,12 +1,14 @@ import React from 'react'; import { Layout, Nav, Button, Avatar, Icon } from '@douyinfe/semi-ui'; -import { IconBell, IconHelpCircle, IconGithubLogo, IconHome, IconHistogram, IconLive, IconSetting, IconSemiLogo } from '@douyinfe/semi-icons'; -import { Gateway } from '../components/icon'; +import { IconBell, IconHelpCircle, IconGithubLogo, IconHome, IconServerStroked, IconLive, IconSetting, } from '@douyinfe/semi-icons'; +import { Outlet,useNavigate } from 'react-router-dom'; export default function MainLayout() { const [selectedKeys, setSelectedKeys] = React.useState(['Home']); const { Header, Footer, Sider, Content } = Layout; + const navigate = useNavigate(); + return ( @@ -14,11 +16,20 @@ export default function MainLayout() { defaultSelectedKeys={selectedKeys} onSelect={key => { setSelectedKeys([key.itemKey.toString()]); + if (key.itemKey === 'Home') { + navigate('/'); + } else if (key.itemKey === 'Gateway') { + navigate('/Gateway'); + } else if (key.itemKey === 'Logger') { + navigate('/Logger'); + } else if (key.itemKey === 'Setting') { + navigate('/Setting'); + } }} style={{ maxWidth: 220, height: '100%' }} items={[ { itemKey: 'Home', text: '首页', icon: }, - { itemKey: 'Gateway', text: '代理设置', icon: }> }, + { itemKey: 'Gateway', text: '代理设置', icon: }, { itemKey: 'Logger', text: '日志', icon: }, { itemKey: 'Setting', text: '设置', icon: }, ]} @@ -37,24 +48,9 @@ export default function MainLayout() { mode="horizontal" footer={ <> -