修复 token无法校验问题

This commit is contained in:
小肥羊 2025-08-04 10:45:53 +08:00
parent 7075ec039d
commit 658b75e3ad
17 changed files with 279 additions and 48 deletions

View File

@ -17,7 +17,7 @@ namespace Learn.Archives.API.Controllers
this.baseService = baseService;
}
/// <summary>
/// 后台管理员登录
/// 管理员登录
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
@ -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),
]);
}
}
}

View File

@ -0,0 +1,15 @@
using Learn.Archives.Core.Model;
namespace Learn.Archives.API.Controllers.Dto
{
/// <summary>
/// 菜单树
/// </summary>
public class MenuTree : Menu
{
/// <summary>
/// 子菜单列表
/// </summary>
public MenuTree[] Children { get; set; } = Array.Empty<MenuTree>();
}
}

View File

@ -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
{
/// <summary>
/// 路由菜单
/// </summary>
public class MenuController : BackController<Menu>
{
readonly Repository<Menu> baseService;
readonly Repository<MenuRelation> menuRelationDB;
readonly LiveUserInfo userInfo;
public MenuController(Repository<Menu> baseService, LiveUserInfo userInfo, Repository<MenuRelation> menuRelationDB) : base(baseService)
{
this.baseService = baseService;
this.userInfo = userInfo;
this.menuRelationDB = menuRelationDB;
}
/// <summary>
/// 管理员菜单
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<MenuTree[]> AdminMenu()
{
var rId = userInfo.RoleId;
if (rId == 0) Oh.Error("登录了无效的用户");
var menuArr = await menuRelationDB.AsQueryable()
.LeftJoin<Menu>((mr, m) => mr.MenuId == m.Id)
.Where((mr,m)=> mr.RoleId == userInfo.RoleId)
.Select((mr, m) => m)
.ToArrayAsync();
return GetChildren(menuArr, menuArr.First().Id);
}
/// <summary>
/// 递归获取子菜单
/// </summary>
[NonAction]
private static MenuTree[] GetChildren(IEnumerable<Menu> 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();
}
}
}

View File

@ -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
/// </summary>
[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<T> BaseQuery(QueryDto model)
{
List<IConditionalModel> where = new List<IConditionalModel>();
foreach (var item in model.Conditions)
where.Add(item);
List<IConditionalModel> 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<IConditionalModel> 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<ComboModel>($"{model.TextName} as Text , {model.ValueName} as Value").ToListAsync();
return res;
}

View File

@ -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;
}

View File

@ -10,8 +10,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.13.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
</ItemGroup>

View File

@ -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<BasicAuthMiddleware>("Swagger");
app.UseSwagger();
app.UseSwaggerUI();
app.UseRouting();
//自定义 应用
app.UseCorsExpand();
app.UseSqlSugarExpand();

View File

@ -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<string, string> FormulaData;
static string FormulaDataKey;
/// <summary>
/// 帧文件名称
/// </summary>
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(); // 或者保留大写
}
}
/// <summary>
/// 对象转化为JSON字符串

View File

@ -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<Admin> 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<bool> 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<Repository<Admin>>();
//}
//if (baseservice == null) return false;
var admin = await DbScoped.Sugar.Queryable<Admin>()
.FirstAsync(x => x.Account == username);
if (admin == null || !admin!.Enable) return false;
else if (admin.Password != password.GetMD5()) return false;
else return true;
}
}
}

View File

@ -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<SwaggerFileUploadFilter>();
//按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<ApiExplorerSettingsAttribute>().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<string>()
}
});
//添加一个必须的全局安全信息和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<string, OpenApiSchema> schema = new Dictionary<string, OpenApiSchema>();
schema["fileName"] = new OpenApiSchema { Description = "选择上传文件", Type = "string", Format = "binary" };
Dictionary<string, OpenApiMediaType> content = new Dictionary<string, OpenApiMediaType>();
content["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "object", Properties = schema } };
operation.RequestBody = new OpenApiRequestBody() { Content = content };
}
}
}
}

View File

@ -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)
{

View File

@ -32,9 +32,9 @@ namespace Learn.Archives.Core.Common
/// <summary>
/// 管理员角色id
/// </summary>
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");
}
/// <summary>

View File

@ -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
{

View File

@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="SqlSugar.IOC" Version="2.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.170" />
<PackageReference Include="UserCenter.Model" Version="1.3.9" />

View File

@ -33,7 +33,7 @@ namespace Learn.Archives.Core.Model
/// <summary>
/// 密码
/// </summary>
[SugarColumn(Length = 12)]
[SugarColumn(Length = 32)]
public string Password { get; set; }
/// <summary>
/// 启用

View File

@ -19,15 +19,16 @@ namespace Learn.Archives.Core.Model
/// 名称
/// </summary>
[SugarColumn(Length = 50)]
public required string Name { get; set; }
public string Name { get; set; }
/// <summary>
/// 标题
/// </summary>
[SugarColumn(Length = 20)]
public required string Title { get; set; }
public string Title { get; set; }
/// <summary>
/// 路径
/// </summary>
[SugarColumn(IsNullable = true)]
public string? Path { get; set; }
/// <summary>
/// 是按钮权限
@ -36,10 +37,12 @@ namespace Learn.Archives.Core.Model
/// <summary>
/// 图标
/// </summary>
[SugarColumn(IsNullable = true)]
public string? Icon { get; set; }
/// <summary>
/// 需要的授权码
/// </summary>
[SugarColumn(IsNullable = true)]
public string? Auths { get; set; }
/// <summary>
/// 排名
@ -51,5 +54,12 @@ namespace Learn.Archives.Core.Model
/// </summary>
public bool ShowLink { get; set; }
/// <summary>
/// 父级菜单ID
/// <para>属于<see cref="Menu.Id"/></para>
/// </summary>
[SugarColumn(IsNullable = true)]
public long ParentId { get; set; }
}
}

View File

@ -18,11 +18,11 @@ namespace Learn.Archives.Core.Model
/// <summary>
/// 菜单id
/// </summary>
public required long MenuId { get; set; }
public long MenuId { get; set; }
/// <summary>
/// 管理员id
/// 角色id
/// </summary>
public required long AdminId { get; set; }
public long RoleId { get; set; }
/// <summary>
/// 创建时间