From 658b75e3ad6f64d148bc5bb7d52c16a162977331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Mon, 4 Aug 2025 10:45:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20token=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AdminController.cs | 6 +- Learn.Archives.API/Controllers/Dto/MeunDto.cs | 15 +++ .../Controllers/MenuController.cs | 71 ++++++++++++ .../{BaseController.cs => _BaseController.cs} | 14 +-- Learn.Archives.API/Expand/AuthorizeExpand.cs | 21 +++- Learn.Archives.API/Learn.Archives.API.csproj | 4 +- Learn.Archives.API/Program.cs | 17 +-- Learn.Archives.Core/Common/AppCommon.cs | 17 ++- .../Common/BasicAuthMiddleware.cs | 26 ++++- .../Common/Expand/SwaggerExpand.cs | 106 ++++++++++++++++++ Learn.Archives.Core/Common/JwtHelper.cs | 2 +- Learn.Archives.Core/Common/LiveUserInfo.cs | 4 +- Learn.Archives.Core/Common/RedisExpand.cs | 1 - .../Learn.Archives.Core.csproj | 1 + Learn.Archives.Core/Model/Admin.cs | 2 +- Learn.Archives.Core/Model/Menu.cs | 14 ++- Learn.Archives.Core/Model/MenuRelation.cs | 6 +- 17 files changed, 279 insertions(+), 48 deletions(-) create mode 100644 Learn.Archives.API/Controllers/Dto/MeunDto.cs create mode 100644 Learn.Archives.API/Controllers/MenuController.cs rename Learn.Archives.API/Controllers/{BaseController.cs => _BaseController.cs} (90%) create mode 100644 Learn.Archives.Core/Common/Expand/SwaggerExpand.cs diff --git a/Learn.Archives.API/Controllers/AdminController.cs b/Learn.Archives.API/Controllers/AdminController.cs index 15dc819..6372ffc 100644 --- a/Learn.Archives.API/Controllers/AdminController.cs +++ b/Learn.Archives.API/Controllers/AdminController.cs @@ -17,7 +17,7 @@ namespace Learn.Archives.API.Controllers this.baseService = baseService; } /// - /// 后台管理员登录 + /// 管理员登录 /// /// /// @@ -36,7 +36,7 @@ namespace Learn.Archives.API.Controllers Oh.Error("登录失败,用户不存在!"); if (!admin!.Enable) Oh.Error("登录失败,用户已锁定!"); - if (admin.Password != model.Password) + if (admin.Password != model.Password.GetMD5()) Oh.Error("登录失败,密码错误"); // 获取租户信息 @@ -48,5 +48,7 @@ namespace Learn.Archives.API.Controllers new Claim(ClaimEnum.Name, admin.Name), ]); } + + } } diff --git a/Learn.Archives.API/Controllers/Dto/MeunDto.cs b/Learn.Archives.API/Controllers/Dto/MeunDto.cs new file mode 100644 index 0000000..a5f74af --- /dev/null +++ b/Learn.Archives.API/Controllers/Dto/MeunDto.cs @@ -0,0 +1,15 @@ +using Learn.Archives.Core.Model; + +namespace Learn.Archives.API.Controllers.Dto +{ + /// + /// 菜单树 + /// + public class MenuTree : Menu + { + /// + /// 子菜单列表 + /// + public MenuTree[] Children { get; set; } = Array.Empty(); + } +} diff --git a/Learn.Archives.API/Controllers/MenuController.cs b/Learn.Archives.API/Controllers/MenuController.cs new file mode 100644 index 0000000..49535ac --- /dev/null +++ b/Learn.Archives.API/Controllers/MenuController.cs @@ -0,0 +1,71 @@ +using Learn.Archives.API.Controllers.Dto; +using Learn.Archives.API.Expand; +using Learn.Archives.Core.Common; +using Learn.Archives.Core.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Diagnostics; +using System.Security.Claims; +using UserCenter.Model; + +namespace Learn.Archives.API.Controllers +{ + /// + /// 路由菜单 + /// + public class MenuController : BackController + { + readonly Repository baseService; + readonly Repository menuRelationDB; + readonly LiveUserInfo userInfo; + public MenuController(Repository baseService, LiveUserInfo userInfo, Repository menuRelationDB) : base(baseService) + { + this.baseService = baseService; + this.userInfo = userInfo; + this.menuRelationDB = menuRelationDB; + } + + /// + /// 管理员菜单 + /// + /// + [HttpGet] + public async Task AdminMenu() + { + var rId = userInfo.RoleId; + if (rId == 0) Oh.Error("登录了无效的用户"); + var menuArr = await menuRelationDB.AsQueryable() + .LeftJoin((mr, m) => mr.MenuId == m.Id) + .Where((mr,m)=> mr.RoleId == userInfo.RoleId) + .Select((mr, m) => m) + .ToArrayAsync(); + return GetChildren(menuArr, menuArr.First().Id); + } + /// + /// 递归获取子菜单 + /// + [NonAction] + private static MenuTree[] GetChildren(IEnumerable menus, long parentId) + { + return menus + .Where(m => m.ParentId == parentId) + .OrderBy(m => m.Rank) + .Select(m => new MenuTree + { + Id = m.Id, + Name = m.Name, + Title = m.Title, + Path = m.Path, + IsButton = m.IsButton, + Icon = m.Icon, + Auths = m.Auths, + Rank = m.Rank, + ShowLink = m.ShowLink, + ParentId = m.ParentId, + Children = GetChildren(menus, m.Id) + }) + .ToArray(); + } + + } +} diff --git a/Learn.Archives.API/Controllers/BaseController.cs b/Learn.Archives.API/Controllers/_BaseController.cs similarity index 90% rename from Learn.Archives.API/Controllers/BaseController.cs rename to Learn.Archives.API/Controllers/_BaseController.cs index 9f8e14d..0e517db 100644 --- a/Learn.Archives.API/Controllers/BaseController.cs +++ b/Learn.Archives.API/Controllers/_BaseController.cs @@ -12,7 +12,7 @@ using UserCenter.Model.Interface; namespace Learn.Archives.API.Controllers { [ApiController] - public class BaseController : ControllerBase + public class _BaseController : ControllerBase { } @@ -21,7 +21,7 @@ namespace Learn.Archives.API.Controllers /// [Authorize(AuthenticationSchemes = Authentication.Admin)] [Route("api/[controller]/[action]")] - public abstract class BackBaseController : BaseController + public abstract class BackBaseController : _BaseController { } @@ -108,9 +108,7 @@ namespace Learn.Archives.API.Controllers [NonAction] public virtual ISugarQueryable BaseQuery(QueryDto model) { - List where = new List(); - foreach (var item in model.Conditions) - where.Add(item); + List where = [.. model.Conditions]; var d = _baseRepository.AsQueryable() .Where(where); if ((typeof(T)).GetProperty("DeleteState") != null) @@ -143,11 +141,7 @@ namespace Learn.Archives.API.Controllers { if (string.IsNullOrEmpty(model.ValueName) || string.IsNullOrEmpty(model.TextName)) Oh.ModelError("ValueName TextName 是必填项"); - List where = [.. model.Conditions]; - var sqlquery = _baseRepository.AsQueryable().Where(where).Where("DeleteState=0"); - - if (!string.IsNullOrEmpty(model.OrderBy)) - sqlquery = sqlquery.OrderByPropertyName(model.OrderBy, model.OrderByType); + var sqlquery = BaseQuery(model); var res = await sqlquery.Select($"{model.TextName} as Text , {model.ValueName} as Value").ToListAsync(); return res; } diff --git a/Learn.Archives.API/Expand/AuthorizeExpand.cs b/Learn.Archives.API/Expand/AuthorizeExpand.cs index 78a5964..9008358 100644 --- a/Learn.Archives.API/Expand/AuthorizeExpand.cs +++ b/Learn.Archives.API/Expand/AuthorizeExpand.cs @@ -7,6 +7,7 @@ using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Learn.Archives.Core.Model.Dto; using Aliyun.OSS; +using NetTaste; namespace Learn.Archives.API.Expand { @@ -18,6 +19,7 @@ namespace Learn.Archives.API.Expand .AddJwtBearer(Authentication.Admin, options => { options.RequireHttpsMetadata = false; + options.UseSecurityTokenValidators = true; options.TokenValidationParameters = new TokenValidationParameters { SaveSigninToken = false,//保存token,后台验证token是否生效(重要) @@ -30,6 +32,19 @@ namespace Learn.Archives.API.Expand }; options.Events = new JwtBearerEvents { + OnMessageReceived = context => + { + var token = context.Request.Headers["Authorization"].FirstOrDefault(); + // 3. 安全提取令牌 + if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + // 移除"Bearer "前缀并清除两端空格 + token = token.Substring("Bearer ".Length).Trim(); + context.Token = token; + } + + return Task.CompletedTask; + }, OnAuthenticationFailed = context => { context.Response.Clear(); @@ -41,11 +56,13 @@ namespace Learn.Archives.API.Expand }, OnChallenge = context => { + if(context.Response.StatusCode == 403 || context.Response.StatusCode == 401) + return Task.CompletedTask; context.HandleResponse(); context.Response.Clear(); context.Response.ContentType = "application/json"; - context.Response.StatusCode = 403; - var data = new BaseReturn() { Code = 403, Message = context.Error + context.AuthenticateFailure?.Message }; + context.Response.StatusCode = 401; + var data = new BaseReturn() { Code = 401, Message = context.Error + context.AuthenticateFailure?.Message }; context.Response.WriteAsync(data.ToJson()); return Task.CompletedTask; } diff --git a/Learn.Archives.API/Learn.Archives.API.csproj b/Learn.Archives.API/Learn.Archives.API.csproj index 063b96d..13893db 100644 --- a/Learn.Archives.API/Learn.Archives.API.csproj +++ b/Learn.Archives.API/Learn.Archives.API.csproj @@ -10,8 +10,10 @@ + + - + diff --git a/Learn.Archives.API/Program.cs b/Learn.Archives.API/Program.cs index dd955f5..bdd7f8d 100644 --- a/Learn.Archives.API/Program.cs +++ b/Learn.Archives.API/Program.cs @@ -28,18 +28,7 @@ builder.Services.AddControllers(options => options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// ĬСշ null շ }); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => -{ - c.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Description = "ѧУϵͳv1" - }); - var file = Path.Combine(AppContext.BaseDirectory, "Learn.Archives.API.xml"); // xmlĵ· - c.IncludeXmlComments(file, true); // true : ʾע - c.OrderActionsBy(o => o.RelativePath); // actionƽжͿԿЧˡ -}); - +builder.Services.AddSwaggerExpand("ѧУϵͳ"); builder.Configuration.AddAppConfig(args); builder.Services.AddPermissionAuthentication(); builder.Services.AddSqlSugarExpand(); @@ -54,12 +43,16 @@ builder.Services.AddHttpContextAccessor(); var app = builder.Build(); +AppCommon.Services = app.Services; + app.UseMiddleware("Swagger"); app.UseSwagger(); app.UseSwaggerUI(); +app.UseRouting(); + //Զ Ӧ app.UseCorsExpand(); app.UseSqlSugarExpand(); diff --git a/Learn.Archives.Core/Common/AppCommon.cs b/Learn.Archives.Core/Common/AppCommon.cs index 22b3db2..2d35d2c 100644 --- a/Learn.Archives.Core/Common/AppCommon.cs +++ b/Learn.Archives.Core/Common/AppCommon.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; using System.Runtime.Loader; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -70,12 +71,16 @@ namespace Learn.Archives.Core.Common public static class ExpandFunction { - static Dictionary FormulaData; - static string FormulaDataKey; - /// - /// 帧文件名称 - /// - public static string FrameName = "frame_"; + + + public static string GetMD5(this string input) + { + using (MD5 md5 = MD5.Create()) + { + byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(hashBytes).ToUpper(); // 或者保留大写 + } + } /// /// 对象转化为JSON字符串 diff --git a/Learn.Archives.Core/Common/BasicAuthMiddleware.cs b/Learn.Archives.Core/Common/BasicAuthMiddleware.cs index b4bd09d..9414eec 100644 --- a/Learn.Archives.Core/Common/BasicAuthMiddleware.cs +++ b/Learn.Archives.Core/Common/BasicAuthMiddleware.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Http; +using Learn.Archives.Core.Model; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using OracleInternal.Secure.Network; +using SqlSugar.IOC; using System; using System.Text; using System.Threading.Tasks; @@ -12,6 +14,8 @@ namespace Learn.Archives.Core.Common private readonly RequestDelegate _next; private readonly string _realm; + private Repository baseservice; + public BasicAuthMiddleware(RequestDelegate next, string realm) { _next = next; @@ -20,7 +24,8 @@ namespace Learn.Archives.Core.Common public async Task InvokeAsync(HttpContext context) { - if (context.Request.Path.StartsWithSegments("/swagger")) + if (context.Request.Path.StartsWithSegments("/swagger") + && (context.Request.Path.Value?.Contains("swagger.json") ?? true)) { string authHeader = context.Request.Headers["Authorization"]; if (authHeader != null && authHeader.StartsWith("Basic ")) @@ -31,6 +36,7 @@ namespace Learn.Archives.Core.Common if (await IsAuthorized(usernamePassword[0], usernamePassword[1])) { + await _next(context); return; } @@ -45,9 +51,19 @@ namespace Learn.Archives.Core.Common private async Task IsAuthorized(string username, string password) { - // 在这里验证用户名和密码 - return AppCommon.Config.Admin.Account == username - && AppCommon.Config.Admin.Password == password; + //if (baseservice == null) + //{ + // using var scope = AppCommon.Services?.CreateScope(); + // if (scope != null) + // baseservice = scope.ServiceProvider.GetService>(); + //} + //if (baseservice == null) return false; + + var admin = await DbScoped.Sugar.Queryable() + .FirstAsync(x => x.Account == username); + if (admin == null || !admin!.Enable) return false; + else if (admin.Password != password.GetMD5()) return false; + else return true; } } } diff --git a/Learn.Archives.Core/Common/Expand/SwaggerExpand.cs b/Learn.Archives.Core/Common/Expand/SwaggerExpand.cs new file mode 100644 index 0000000..845d276 --- /dev/null +++ b/Learn.Archives.Core/Common/Expand/SwaggerExpand.cs @@ -0,0 +1,106 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Learn.Archives.Core.Common.Expand +{ + public static class SwaggerExpand + { + public static void AddSwaggerExpand(this IServiceCollection s,string name="") + { + s.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Description = name + }); + c.OperationFilter(); + //按Http类型排序 + c.OrderActionsBy(o => o.GroupName); + + c.DocInclusionPredicate((docName, apiDesc) => + { + try + { + if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; + var versions = methodInfo.DeclaringType.GetCustomAttributes(true) + .OfType().Select(attr => attr.GroupName); + if (docName.ToLower() == "v1" && versions.FirstOrDefault() == null) + return true; + return versions.Any(v => v.ToString() == docName); + } + catch (Exception ex) + { + + throw; + } + + }); + + + //添加全局安全性需求 + c.AddSecurityRequirement(new OpenApiSecurityRequirement{ + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "bearerAuth" + } + }, Array.Empty() + } + }); + + //添加一个必须的全局安全信息,和AddSecurityDefinition方法指定的方案名称要一致,这里是Bearer。 + c.AddSecurityDefinition("bearerAuth", + new OpenApiSecurityScheme + { + Description = "使用JWT授权头。示例:\"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + //内容为以 bearer开头 + Scheme = "bearer", + BearerFormat = "JWT" + }); + + DirectoryInfo dirs = new DirectoryInfo(AppContext.BaseDirectory); + FileInfo[] files = dirs.GetFiles("*.xml"); + foreach (var path in files) + { + c.IncludeXmlComments(path.FullName); + } + }); + //s.AddSwaggerGenNewtonsoftSupport(); + } + } + + + class SwaggerFileUploadFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.ApiDescription.ActionDescriptor.Parameters.Any(w => w.ParameterType == typeof(IFormCollection))) + { + Dictionary schema = new Dictionary(); + schema["fileName"] = new OpenApiSchema { Description = "选择上传文件", Type = "string", Format = "binary" }; + Dictionary content = new Dictionary(); + content["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "object", Properties = schema } }; + operation.RequestBody = new OpenApiRequestBody() { Content = content }; + } + } + } + + +} diff --git a/Learn.Archives.Core/Common/JwtHelper.cs b/Learn.Archives.Core/Common/JwtHelper.cs index 9771fe4..7141fb5 100644 --- a/Learn.Archives.Core/Common/JwtHelper.cs +++ b/Learn.Archives.Core/Common/JwtHelper.cs @@ -33,7 +33,7 @@ namespace Learn.Archives.Core.Common //JWT ID 唯一标识符 new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString() ), //发布时间戳 issued timestamp - new Claim(JwtRegisteredClaimNames.Iat, now.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), }; if (claims != null && claims.Length > 0) { diff --git a/Learn.Archives.Core/Common/LiveUserInfo.cs b/Learn.Archives.Core/Common/LiveUserInfo.cs index 2331887..d9c2ef1 100644 --- a/Learn.Archives.Core/Common/LiveUserInfo.cs +++ b/Learn.Archives.Core/Common/LiveUserInfo.cs @@ -32,9 +32,9 @@ namespace Learn.Archives.Core.Common /// /// 管理员角色id /// - public string? RoleId + public long? RoleId { - get => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimEnum.Role)?.Value; + get => long.Parse(_httpContextAccessor.HttpContext?.User.FindFirst(ClaimEnum.Role)?.Value ?? "0"); } /// diff --git a/Learn.Archives.Core/Common/RedisExpand.cs b/Learn.Archives.Core/Common/RedisExpand.cs index 707b5c9..86c7a4d 100644 --- a/Learn.Archives.Core/Common/RedisExpand.cs +++ b/Learn.Archives.Core/Common/RedisExpand.cs @@ -11,7 +11,6 @@ using System.Threading.Channels; using System.Threading.Tasks; using System.Xml.Linq; using UserCenter.Model.Enum; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Learn.Archives.Core.Common { diff --git a/Learn.Archives.Core/Learn.Archives.Core.csproj b/Learn.Archives.Core/Learn.Archives.Core.csproj index 9c9496e..e36cb52 100644 --- a/Learn.Archives.Core/Learn.Archives.Core.csproj +++ b/Learn.Archives.Core/Learn.Archives.Core.csproj @@ -23,6 +23,7 @@ + diff --git a/Learn.Archives.Core/Model/Admin.cs b/Learn.Archives.Core/Model/Admin.cs index 75a5925..7af3047 100644 --- a/Learn.Archives.Core/Model/Admin.cs +++ b/Learn.Archives.Core/Model/Admin.cs @@ -33,7 +33,7 @@ namespace Learn.Archives.Core.Model /// /// 密码 /// - [SugarColumn(Length = 12)] + [SugarColumn(Length = 32)] public string Password { get; set; } /// /// 启用 diff --git a/Learn.Archives.Core/Model/Menu.cs b/Learn.Archives.Core/Model/Menu.cs index a1cc092..460f591 100644 --- a/Learn.Archives.Core/Model/Menu.cs +++ b/Learn.Archives.Core/Model/Menu.cs @@ -19,15 +19,16 @@ namespace Learn.Archives.Core.Model /// 名称 /// [SugarColumn(Length = 50)] - public required string Name { get; set; } + public string Name { get; set; } /// /// 标题 /// [SugarColumn(Length = 20)] - public required string Title { get; set; } + public string Title { get; set; } /// /// 路径 /// + [SugarColumn(IsNullable = true)] public string? Path { get; set; } /// /// 是按钮权限 @@ -36,10 +37,12 @@ namespace Learn.Archives.Core.Model /// /// 图标 /// + [SugarColumn(IsNullable = true)] public string? Icon { get; set; } /// /// 需要的授权码 /// + [SugarColumn(IsNullable = true)] public string? Auths { get; set; } /// /// 排名 @@ -51,5 +54,12 @@ namespace Learn.Archives.Core.Model /// public bool ShowLink { get; set; } + /// + /// 父级菜单ID + /// 属于 + /// + [SugarColumn(IsNullable = true)] + public long ParentId { get; set; } + } } diff --git a/Learn.Archives.Core/Model/MenuRelation.cs b/Learn.Archives.Core/Model/MenuRelation.cs index efe7bb8..bcb6704 100644 --- a/Learn.Archives.Core/Model/MenuRelation.cs +++ b/Learn.Archives.Core/Model/MenuRelation.cs @@ -18,11 +18,11 @@ namespace Learn.Archives.Core.Model /// /// 菜单id /// - public required long MenuId { get; set; } + public long MenuId { get; set; } /// - /// 管理员id + /// 角色id /// - public required long AdminId { get; set; } + public long RoleId { get; set; } /// /// 创建时间