支持路由细粒度ACL和用户功能

This commit is contained in:
root 2024-12-01 01:27:28 +08:00
parent cf25e63de7
commit 7fe9d05ffa
13 changed files with 1416 additions and 32 deletions

View file

@ -0,0 +1,45 @@
using iFileProxy.Models;
using iFileProxy.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace iFileProxy.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
private readonly UserMask[] _allowedMasks;
public AuthorizeAttribute(params UserMask[] allowedMasks)
{
_allowedMasks = allowedMasks;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var userId = context.HttpContext.Items["User"]?.ToString();
if (string.IsNullOrEmpty(userId))
{
context.Result = new JsonResult(new CommonRsp { Message = "未授权", Retcode = -1 })
{
StatusCode = StatusCodes.Status401Unauthorized
};
return;
}
if (_allowedMasks.Length > 0)
{
var dbGateService = context.HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var user = dbGateService.GetUserByIdAsync(userId).Result;
if (user == null || !_allowedMasks.Contains(user.Mask) )
{
context.Result = new JsonResult(new CommonRsp { Message = "权限不足" ,Retcode = 1})
{
StatusCode = StatusCodes.Status403Forbidden
};
}
}
}
}
}

View file

@ -4,8 +4,10 @@ using iFileProxy.Services;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using System.Text.Json;
using iFileProxy.Attributes;
namespace iFileProxy.Controllers
{
[Authorize(UserMask.Admin,UserMask.SuperAdmin)]
[Route("[controller]")]
[ApiController]
public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService) : ControllerBase

View file

@ -0,0 +1,607 @@
using iFileProxy.Attributes;
using iFileProxy.Models;
using iFileProxy.Services;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace iFileProxy.Controllers
{
[Route("[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly AuthService _authService;
private readonly ILogger<UserController> _logger;
public UserController(AuthService authService, ILogger<UserController> logger)
{
_authService = authService;
_logger = logger;
}
/// <summary>
/// 用户注册
/// </summary>
[HttpPost("register")]
public async Task<ActionResult<CommonRsp>> Register([FromBody] RegisterRequest request)
{
try
{
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var (success, message) = await _authService.RegisterAsync(request.Username, request.Password, ip, request.NickName);
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = message
});
}
catch (Exception ex)
{
_logger.LogError(ex, "注册失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "注册失败"
});
}
}
/// <summary>
/// 用户登录
/// </summary>
[HttpPost("login")]
public async Task<ActionResult<CommonRsp>> Login([FromBody] LoginRequest request)
{
try
{
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var (success, token, message) = await _authService.LoginAsync(request.Username, request.Password, ip);
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = message,
Data = success ? new { token } : null
});
}
catch (Exception ex)
{
_logger.LogError(ex, "登录失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "登录失败"
});
}
}
/// <summary>
/// 获取当前用户信息
/// </summary>
[Authorize]
[HttpGet("info")]
public async Task<ActionResult<CommonRsp>> GetUserInfo()
{
try
{
var userId = HttpContext.Items["User"]?.ToString();
if (string.IsNullOrEmpty(userId))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "未登录"
});
}
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var user = await dbGateService.GetUserByIdAsync(userId);
if (user == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
_logger.LogInformation(JsonConvert.SerializeObject(user));
// 不返回敏感信息
return Ok(new CommonRsp
{
Retcode = 0,
Message = "success",
Data = new
{
user.UserId,
user.Username,
user.Mask,
user.State,
user.CreateTime,
user.LastLoginTime,
user.LastLoginIP,
user.Nickname
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户信息失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "获取用户信息失败"
});
}
}
/// <summary>
/// 修改用户权限
/// </summary>
[Authorize(UserMask.SuperAdmin)]
[HttpPost("updateMask/{userId}")]
public async Task<ActionResult<CommonRsp>> UpdateUserMask(string userId, [FromBody] UpdateUserMaskRequest request)
{
try
{
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
// 获取目标用户
var targetUser = await dbGateService.GetUserByIdAsync(userId);
if (targetUser == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
// 不允许修改超级管理员的权限
if (targetUser.Mask == UserMask.SuperAdmin)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "不能修改超级管理员的权限"
});
}
// 更新权限
targetUser.Mask = request.NewMask;
var success = await dbGateService.UpdateUserAsync(targetUser);
// 记录操作事件
var operatorId = HttpContext.Items["User"]?.ToString();
if (success && !string.IsNullOrEmpty(operatorId))
{
var userEvent = new UserEvent
{
UserId = targetUser.UserId,
EventType = UserEventType.UpdateMask,
EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
EventDetail = $"权限被修改为 {request.NewMask}操作者ID: {operatorId}"
};
await dbGateService.CreateUserEventAsync(userEvent);
}
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = success ? "权限修改成功" : "权限修改失败"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "修改用户权限失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "修改用户权限失败"
});
}
}
/// <summary>
/// 获取用户列表(分页)
/// </summary>
[Authorize(UserMask.SuperAdmin)]
[HttpGet("list")]
public async Task<ActionResult<CommonRsp>> GetUserList([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] UserMask? mask = null)
{
try
{
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var result = await dbGateService.GetPagedUserListAsync(page, pageSize, mask);
// 移除敏感信息
foreach (var user in result.Data)
{
user.PasswordHash = "***************";
}
return Ok(new CommonRsp
{
Retcode = 0,
Message = "success",
Data = result
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户列表失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "获取用户列表失败"
});
}
}
/// <summary>
/// 修改个人昵称
/// </summary>
[Authorize]
[HttpPost("updateNickname")]
public async Task<ActionResult<CommonRsp>> UpdateNickname([FromBody] UpdateNicknameRequest request)
{
try
{
var userId = HttpContext.Items["User"]?.ToString();
if (string.IsNullOrEmpty(userId))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "未登录"
});
}
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var user = await dbGateService.GetUserByIdAsync(userId);
if (user == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
user.Nickname = request.NewNickname;
var success = await dbGateService.UpdateUserAsync(user);
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = success ? "昵称修改成功" : "昵称修改失败"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "修改昵称失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "修改昵称失败"
});
}
}
/// <summary>
/// 修改密码
/// </summary>
[Authorize]
[HttpPost("updatePassword")]
public async Task<ActionResult<CommonRsp>> UpdatePassword([FromBody] UpdatePasswordRequest request)
{
try
{
var userId = HttpContext.Items["User"]?.ToString();
if (string.IsNullOrEmpty(userId))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "未登录"
});
}
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var user = await dbGateService.GetUserByIdAsync(userId);
if (user == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
// 验证旧密码
if (!BCrypt.Net.BCrypt.Verify(request.OldPassword, user.PasswordHash))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "旧密码错误"
});
}
// 更新密码
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
var success = await dbGateService.UpdateUserAsync(user);
// 记录密码修改事件
if (success)
{
var userEvent = new UserEvent
{
UserId = user.UserId,
EventType = UserEventType.UpdatePassword,
EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
EventDetail = "密码已修改"
};
await dbGateService.CreateUserEventAsync(userEvent);
}
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = success ? "密码修改成功" : "密码修改失败"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "修改密码失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "修改密码失败"
});
}
}
/// <summary>
/// 超级管理员修改用户密码
/// </summary>
[Authorize(UserMask.SuperAdmin)]
[HttpPost("resetPassword/{userId}")]
public async Task<ActionResult<CommonRsp>> ResetUserPassword(string userId, [FromBody] ResetPasswordRequest request)
{
try
{
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
// 获取目标用户
var targetUser = await dbGateService.GetUserByIdAsync(userId);
if (targetUser == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
// 不允许修改超级管理员的密码
if (targetUser.Mask == UserMask.SuperAdmin && targetUser.UserId != HttpContext.Items["User"]?.ToString())
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "不能修改其他超级管理员的密码"
});
}
// 更新密码
targetUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
var success = await dbGateService.UpdateUserAsync(targetUser);
// 记录密码重置事件
if (success)
{
var operatorId = HttpContext.Items["User"]?.ToString();
var userEvent = new UserEvent
{
UserId = targetUser.UserId,
EventType = UserEventType.UpdatePassword,
EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
EventDetail = $"密码被管理员重置操作者ID: {operatorId}"
};
await dbGateService.CreateUserEventAsync(userEvent);
}
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = success ? "密码重置成功" : "密码重置失败"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "重置密码失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "重置密码失败"
});
}
}
/// <summary>
/// 获取用户事件列表
/// </summary>
[Authorize]
[HttpGet("events")]
public async Task<ActionResult<CommonRsp>> GetUserEvents(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? keyword = null,
[FromQuery] string? userId = null)
{
try
{
var currentUserId = HttpContext.Items["User"]?.ToString();
if (string.IsNullOrEmpty(currentUserId))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "未登录"
});
}
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
var currentUser = await dbGateService.GetUserByIdAsync(currentUserId);
// 如果不是超级管理员,只能查看自己的事件
if (currentUser?.Mask != UserMask.SuperAdmin)
{
userId = currentUserId;
}
// 如果是管理员且未指定用户ID则查看所有事件
else if (string.IsNullOrEmpty(userId))
{
userId = null; // 查询所有用户的事件
}
var result = await dbGateService.GetPagedUserEventsAsync(page, pageSize, userId, keyword);
return Ok(new CommonRsp
{
Retcode = 0,
Message = "success",
Data = result
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户事件失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "获取用户事件失败"
});
}
}
/// <summary>
/// 管理员修改用户昵称
/// </summary>
[Authorize(UserMask.Admin, UserMask.SuperAdmin)]
[HttpPost("updateNickname/{userId}")]
public async Task<ActionResult<CommonRsp>> UpdateUserNickname(string userId, [FromBody] UpdateNicknameRequest request)
{
try
{
var dbGateService = HttpContext.RequestServices.GetRequiredService<DatabaseGateService>();
// 获取目标用户
var targetUser = await dbGateService.GetUserByIdAsync(userId);
if (targetUser == null)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "用户不存在"
});
}
// 获取当前操作者信息
var operatorId = HttpContext.Items["User"]?.ToString();
var operator_ = await dbGateService.GetUserByIdAsync(operatorId);
// 如果目标用户是超级管理员,只有超级管理员自己能修改自己的昵称
if (targetUser.Mask == UserMask.SuperAdmin &&
(operator_?.Mask != UserMask.SuperAdmin || targetUser.UserId != operatorId))
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "不能修改超级管理员的昵称"
});
}
// 如果目标用户是管理员,只有超级管理员能修改
if (targetUser.Mask == UserMask.Admin && operator_?.Mask != UserMask.SuperAdmin)
{
return Ok(new CommonRsp
{
Retcode = 1,
Message = "只有超级管理员能修改管理员的昵称"
});
}
// 更新昵称
targetUser.Nickname = request.NewNickname;
var success = await dbGateService.UpdateUserAsync(targetUser);
// 记录操作事件
if (success)
{
var userEvent = new UserEvent
{
UserId = targetUser.UserId,
EventType = UserEventType.UpdateNickname, // 需要添加这个事件类型
EventIP = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
EventDetail = $"昵称被修改为 {request.NewNickname}操作者ID: {operatorId}"
};
await dbGateService.CreateUserEventAsync(userEvent);
}
return Ok(new CommonRsp
{
Retcode = success ? 0 : 1,
Message = success ? "昵称修改成功" : "昵称修改失败"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "修改用户昵称失败");
return Ok(new CommonRsp
{
Retcode = 1,
Message = "修改用户昵称失败"
});
}
}
public class UpdateUserMaskRequest
{
public UserMask NewMask { get; set; }
}
public class UpdateNicknameRequest
{
public string NewNickname { get; set; } = string.Empty;
}
public class UpdatePasswordRequest
{
public string OldPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}
public class ResetPasswordRequest
{
public string NewPassword { get; set; } = string.Empty;
}
}
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string NickName { get; set; } = string.Empty;
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,58 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace iFileProxy.Middleware
{
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public JwtMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
await AttachUserToContext(context, token);
await _next(context);
}
private async Task AttachUserToContext(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = jwtToken.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
// 将用户信息附加到上下文中
context.Items["User"] = userId;
}
catch
{
// 令牌验证失败,不附加用户信息
}
}
}
}

View file

@ -6,7 +6,7 @@
public class CommonRsp
{
public string Message { get; set; } = "def msg";
public object Data { get; set; } = new object();
public object Data { get; set; } = null;
public int Retcode { get; set; }
}
}

135
src/Models/User.cs Normal file
View file

@ -0,0 +1,135 @@
using Newtonsoft.Json;
namespace iFileProxy.Models
{
public enum UserMask
{
User = 0,
Admin = 1,
SuperAdmin = 2 // 超级管理员
}
public enum UserState
{
None = 0,
Blocked = 1
}
public enum UserEventType
{
Login = 0,
Logout = 1,
Registry = 2,
UpdateMask = 3, // 添加权限修改事件类型
UpdatePassword = 4, // 添加密码修改事件类型
UpdateNickname = 5, // 添加昵称修改事件类型
}
public class User
{
/// <summary>
/// 用户ID
/// </summary>
[JsonProperty("user_id")]
public string UserId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 昵称
/// </summary>
[JsonProperty("nickname")]
public string Nickname { get; set; } = string.Empty;
/// <summary>
/// 用户名
/// </summary>
[JsonProperty("username")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// 密码哈希
/// </summary>
[JsonProperty("password_hash")]
public string PasswordHash { get; set; } = string.Empty;
/// <summary>
/// 用户权限掩码
/// </summary>
[JsonProperty("mask")]
public UserMask Mask { get; set; } = UserMask.User;
/// <summary>
/// 用户状态
/// </summary>
[JsonProperty("state")]
public UserState State { get; set; } = UserState.None;
/// <summary>
/// 创建时间
/// </summary>
[JsonProperty("create_time")]
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 最后登录时间
/// </summary>
[JsonProperty("last_login_time")]
public DateTime LastLoginTime { get; set; }
/// <summary>
/// 最后登录IP
/// </summary>
[JsonProperty("last_login_ip")]
public string LastLoginIP { get; set; } = string.Empty;
}
public class UserEvent
{
/// <summary>
/// 事件ID
/// </summary>
[JsonProperty("event_id")]
public string EventId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 用户ID
/// </summary>
[JsonProperty("user_id")]
public string UserId { get; set; } = string.Empty;
/// <summary>
/// 事件类型
/// </summary>
[JsonProperty("event_type")]
public UserEventType EventType { get; set; }
/// <summary>
/// 事件时间
/// </summary>
[JsonProperty("event_time")]
public DateTime EventTime { get; set; } = DateTime.Now;
/// <summary>
/// 事件IP
/// </summary>
[JsonProperty("event_ip")]
public string EventIP { get; set; } = string.Empty;
/// <summary>
/// 事件详情
/// </summary>
[JsonProperty("event_detail")]
public string EventDetail { get; set; } = string.Empty;
}
public class UserEventDetail : UserEvent
{
/// <summary>
/// 用户名
/// </summary>
[JsonProperty("username")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// 昵称
/// </summary>
[JsonProperty("nickname")]
public string Nickname { get; set; } = string.Empty;
}
}

View file

@ -3,6 +3,9 @@ using iFileProxy.Helpers;
using iFileProxy.Middleware;
using iFileProxy.Services;
using Serilog;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace iFileProxy
{
@ -24,7 +27,7 @@ namespace iFileProxy
builder =>
{
builder
.WithOrigins("http://localhost:3000") // ゴ傷華硊
.WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050") // 前端地址
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
@ -52,6 +55,26 @@ namespace iFileProxy
builder.Services.AddSingleton<TaskManager>();
// 添加认证服务
builder.Services.AddScoped<AuthService>();
// 添加JWT认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
var app = builder.Build();
AppConfig.CheckAppConfig(app.Services);
@ -97,6 +120,9 @@ namespace iFileProxy
app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(),
AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP);
// 添加中间件
app.UseMiddleware<JwtMiddleware>();
app.MapControllers();
app.Run();

View file

@ -15,7 +15,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<_TargetId>Folder</_TargetId>
<SiteUrlToLaunchAfterPublish />
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<ProjectGuid>e343bd8a-27ed-47e2-b50d-e3000730e65e</ProjectGuid>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>

141
src/Services/AuthService.cs Normal file
View file

@ -0,0 +1,141 @@
using BCrypt.Net;
using iFileProxy.Models;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace iFileProxy.Services
{
public class AuthService
{
private readonly DatabaseGateService _dbGateService;
private readonly IConfiguration _configuration;
private readonly ILogger<AuthService> _logger;
public AuthService(DatabaseGateService dbGateService, IConfiguration configuration, ILogger<AuthService> logger)
{
_dbGateService = dbGateService;
_configuration = configuration;
_logger = logger;
}
public async Task<(bool success, string message)> RegisterAsync(string username, string password, string ip, string nickname)
{
try
{
// 检查用户名是否已存在
if (await _dbGateService.UserExistsAsync(username))
{
return (false, "用户名已存在");
}
// 检查是否是第一个用户
var isFirstUser = await _dbGateService.GetUserCountAsync() == 0;
// 创建新用户
var user = new User
{
Username = username,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
LastLoginIP = ip,
// 如果是第一个用户,设置为超级管理员
Mask = isFirstUser ? UserMask.SuperAdmin : UserMask.User,
Nickname = nickname
};
// 保存用户
await _dbGateService.CreateUserAsync(user);
// 记录注册事件
var userEvent = new UserEvent
{
UserId = user.UserId,
EventType = UserEventType.Registry,
EventIP = ip,
EventDetail = isFirstUser ? "管理员注册" : "用户注册"
};
await _dbGateService.CreateUserEventAsync(userEvent);
return (true, isFirstUser ? "管理员注册成功" : "注册成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "用户注册失败");
return (false, "注册失败");
}
}
public async Task<(bool success, string token, string message)> LoginAsync(string username, string password, string ip)
{
try
{
var user = await _dbGateService.GetUserByUsernameAsync(username);
if (user == null)
{
return (false, string.Empty, "用户不存在");
}
if (user.State == UserState.Blocked)
{
return (false, string.Empty, "账户已被封禁");
}
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
{
return (false, string.Empty, "密码错误");
}
// 更新登录信息
user.LastLoginTime = DateTime.Now;
user.LastLoginIP = ip;
await _dbGateService.UpdateUserAsync(user);
// 记录登录事件
var userEvent = new UserEvent
{
UserId = user.UserId,
EventType = UserEventType.Login,
EventIP = ip,
EventDetail = "用户登录"
};
await _dbGateService.CreateUserEventAsync(userEvent);
// 生成JWT令牌
var token = GenerateJwtToken(user);
return (true, token, "登录成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "用户登录失败");
return (false, string.Empty, "登录失败");
}
}
private string GenerateJwtToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserId),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Mask.ToString())
};
// 确保密钥至少32字节长
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ??
"iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"] ?? "iFileProxy",
audience: _configuration["Jwt:Audience"] ?? "iFileProxy.Client",
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

View file

@ -6,6 +6,7 @@ using MySql.Data.MySqlClient;
using iFileProxy.Models;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;
namespace iFileProxy.Services
{
@ -17,6 +18,52 @@ namespace iFileProxy.Services
Dictionary<string, DB> _dbDictionary = [];
private const string CREATE_TASKS_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS `t_tasks_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '',
`tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`client_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IP',
`add_time` datetime NOT NULL COMMENT '',
`update_time` datetime NOT NULL COMMENT '',
`status` int(11) NULL DEFAULT NULL COMMENT '',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'url',
`size` int(11) NULL DEFAULT NULL COMMENT '',
`hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'hash',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
""";
private const string CREATE_USERS_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS `t_users` (
`user_id` varchar(36) NOT NULL,
`username` varchar(50) NOT NULL,
`nickname` varchar(50) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`mask` int NOT NULL DEFAULT 0,
`state` int NOT NULL DEFAULT 0,
`create_time` datetime NOT NULL,
`last_login_time` datetime NULL,
`last_login_ip` varchar(45) NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""";
private const string CREATE_USER_EVENTS_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS `t_user_events` (
`event_id` varchar(36) NOT NULL,
`user_id` varchar(36) NOT NULL,
`event_type` int NOT NULL,
`event_time` datetime NOT NULL,
`event_ip` varchar(45) NULL,
`event_detail` text NULL,
PRIMARY KEY (`event_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `fk_user_events_user` FOREIGN KEY (`user_id`) REFERENCES `t_users` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""";
public DatabaseGateService(AppConfig appConfig)
{
_logger.Information("Initializing DatabaseGateService...");
@ -113,9 +160,11 @@ namespace iFileProxy.Services
/// <returns></returns>
public static string QueryTableData(MySqlCommand sqlCmd)
{
DataTable dataTable = new();
using (MySqlDataAdapter adapter = new (sqlCmd))
adapter.Fill(dataTable);
var dataTable = new DataTable();
using (var adapter = new MySqlDataAdapter(sqlCmd))
{
adapter.Fill(dataTable);
}
return JsonConvert.SerializeObject(dataTable);
}
@ -367,37 +416,24 @@ namespace iFileProxy.Services
public void TryInitialDB()
{
string sql = """
CREATE TABLE `t_tasks_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '',
`tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`client_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IP',
`add_time` datetime NOT NULL COMMENT '',
`update_time` datetime NOT NULL COMMENT '',
`status` int(11) NULL DEFAULT NULL COMMENT '',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'url',
`size` int(11) NULL DEFAULT NULL COMMENT '',
`hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'hash',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
""";
MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db");
using var conn = GetAndOpenDBConn("iFileProxy_Db");
try
{
using MySqlCommand cmd = new(sql, conn);
cmd.ExecuteNonQuery();
// 创建表
using (var cmd = new MySqlCommand(CREATE_TASKS_TABLE_SQL, conn))
cmd.ExecuteNonQuery();
using (var cmd = new MySqlCommand(CREATE_USERS_TABLE_SQL, conn))
cmd.ExecuteNonQuery();
using (var cmd = new MySqlCommand(CREATE_USER_EVENTS_TABLE_SQL, conn))
cmd.ExecuteNonQuery();
_logger.Information("数据库表结构初始化完成");
}
catch (Exception)
catch (Exception ex)
{
_logger.Error($"Db Init Fai.l");
_logger.Error($"数据库初始化失败: {ex.Message}");
throw;
}
finally
{
conn.Close();
}
}
public T ExecuteScalar<T>(string sql, Dictionary<string, object> parameters)
{
@ -574,5 +610,265 @@ namespace iFileProxy.Services
throw;
}
}
public async Task<User?> GetUserByIdAsync(string userId)
{
var sql = "SELECT * FROM t_users WHERE user_id = @userId";
var parameters = new Dictionary<string, object>
{
{ "@userId", userId }
};
var users = await ExecuteQueryAsync<User>(sql, parameters);
return users.FirstOrDefault();
}
public async Task<bool> UserExistsAsync(string username)
{
var sql = "SELECT COUNT(*) FROM t_users WHERE username = @username";
var parameters = new Dictionary<string, object>
{
{ "@username", username }
};
var count = await ExecuteScalarAsync<long>(sql, parameters);
return count > 0;
}
public async Task<bool> CreateUserAsync(User user)
{
var sql = @"INSERT INTO t_users (user_id, username, password_hash, mask, state, create_time, last_login_time, last_login_ip, nickname)
VALUES (@userId, @username, @passwordHash, @mask, @state, @createTime, @lastLoginTime, @lastLoginIp, @nickname)";
var parameters = new Dictionary<string, object>
{
{ "@userId", user.UserId },
{ "@username", user.Username },
{ "@passwordHash", user.PasswordHash },
{ "@mask", (int)user.Mask },
{ "@state", (int)user.State },
{ "@createTime", user.CreateTime },
{ "@lastLoginTime", user.LastLoginTime },
{ "@lastLoginIp", user.LastLoginIP },
{ "@nickname", user.Nickname },
};
var result = await ExecuteNonQueryAsync(sql, parameters);
return result > 0;
}
public async Task<bool> CreateUserEventAsync(UserEvent userEvent)
{
var sql = @"INSERT INTO t_user_events (event_id, user_id, event_type, event_time, event_ip, event_detail)
VALUES (@eventId, @userId, @eventType, @eventTime, @eventIp, @eventDetail)";
var parameters = new Dictionary<string, object>
{
{ "@eventId", userEvent.EventId },
{ "@userId", userEvent.UserId },
{ "@eventType", (int)userEvent.EventType },
{ "@eventTime", userEvent.EventTime },
{ "@eventIp", userEvent.EventIP },
{ "@eventDetail", userEvent.EventDetail }
};
var result = await ExecuteNonQueryAsync(sql, parameters);
return result > 0;
}
public async Task<User?> GetUserByUsernameAsync(string username)
{
var sql = "SELECT * FROM t_users WHERE username = @username";
var parameters = new Dictionary<string, object>
{
{ "@username", username }
};
var users = await ExecuteQueryAsync<User>(sql, parameters);
return users.FirstOrDefault();
}
public async Task<bool> UpdateUserAsync(User user)
{
var sql = @"UPDATE t_users SET
username = @username,
password_hash = @passwordHash,
mask = @mask,
state = @state,
last_login_time = @lastLoginTime,
last_login_ip = @lastLoginIp,
nickname = @nickname
WHERE user_id = @userId";
var parameters = new Dictionary<string, object>
{
{ "@userId", user.UserId },
{ "@username", user.Username },
{ "@passwordHash", user.PasswordHash },
{ "@mask", (int)user.Mask },
{ "@state", (int)user.State },
{ "@lastLoginTime", user.LastLoginTime },
{ "@lastLoginIp", user.LastLoginIP },
{ "@nickname", user.Nickname }
};
var result = await ExecuteNonQueryAsync(sql, parameters);
return result > 0;
}
private async Task<T> ExecuteScalarAsync<T>(string sql, Dictionary<string, object> parameters)
{
using var conn = GetAndOpenDBConn("iFileProxy_Db");
using var cmd = new MySqlCommand(sql, conn);
foreach (var param in parameters)
{
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
return (T)await cmd.ExecuteScalarAsync();
}
private async Task<List<T>> ExecuteQueryAsync<T>(string sql, Dictionary<string, object> parameters)
{
using var conn = GetAndOpenDBConn("iFileProxy_Db");
using var cmd = new MySqlCommand(sql, conn);
foreach (var param in parameters)
{
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
// 使用 DataTable 来避免 DataReader 冲突
var dataTable = new DataTable();
using (var adapter = new MySqlDataAdapter(cmd))
{
await Task.Run(() => adapter.Fill(dataTable));
}
return JsonConvert.DeserializeObject<List<T>>(
JsonConvert.SerializeObject(dataTable)
);
}
private async Task<int> ExecuteNonQueryAsync(string sql, Dictionary<string, object> parameters)
{
using var conn = GetAndOpenDBConn("iFileProxy_Db");
using var cmd = new MySqlCommand(sql, conn);
foreach (var param in parameters)
{
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
return await cmd.ExecuteNonQueryAsync();
}
public async Task<long> GetUserCountAsync()
{
var sql = "SELECT COUNT(*) FROM t_users";
var parameters = new Dictionary<string, object>();
return await ExecuteScalarAsync<long>(sql, parameters);
}
public async Task<PagedResult<User>> GetPagedUserListAsync(int page, int pageSize, UserMask? mask = null)
{
// 构建基础SQL
var sql = new StringBuilder("SELECT * FROM t_users");
var parameters = new Dictionary<string, object>();
// 添加权限过滤
if (mask.HasValue)
{
sql.Append(" WHERE mask = @mask");
parameters.Add("@mask", (int)mask.Value);
}
// 添加分页
sql.Append(" ORDER BY create_time DESC LIMIT @offset, @limit");
parameters.Add("@offset", (page - 1) * pageSize);
parameters.Add("@limit", pageSize);
// 获取总记录数
var countSql = "SELECT COUNT(*) FROM t_users" +
(mask.HasValue ? " WHERE mask = @mask" : "");
var totalCount = await ExecuteScalarAsync<long>(countSql, parameters);
// 获取分页数据
var users = await ExecuteQueryAsync<User>(sql.ToString(), parameters);
return new PagedResult<User>
{
Total = totalCount,
PageSize = pageSize,
CurrentPage = page,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
Data = users
};
}
public async Task<PagedResult<UserEventDetail>> GetPagedUserEventsAsync(
int page,
int pageSize,
string? userId = null,
string? keyword = null)
{
// 构建基础SQL
var sql = new StringBuilder(@"
SELECT e.*, u.username, u.nickname
FROM t_user_events e
LEFT JOIN t_users u ON e.user_id = u.user_id
WHERE 1=1");
var parameters = new Dictionary<string, object>();
// 添加用户ID过滤
if (!string.IsNullOrEmpty(userId))
{
sql.Append(" AND e.user_id = @userId");
parameters.Add("@userId", userId);
}
// 添加关键词搜索
if (!string.IsNullOrEmpty(keyword))
{
sql.Append(@" AND (
e.event_detail LIKE @keyword OR
u.username LIKE @keyword OR
u.nickname LIKE @keyword OR
e.event_ip LIKE @keyword
)");
parameters.Add("@keyword", $"%{keyword}%");
}
// 添加分页
sql.Append(" ORDER BY e.event_time DESC LIMIT @offset, @limit");
parameters.Add("@offset", (page - 1) * pageSize);
parameters.Add("@limit", pageSize);
// 获取总记录数
var countSql = @"
SELECT COUNT(*)
FROM t_user_events e
LEFT JOIN t_users u ON e.user_id = u.user_id
WHERE 1=1" +
(!string.IsNullOrEmpty(userId) ? " AND e.user_id = @userId" : "") +
(!string.IsNullOrEmpty(keyword) ? @" AND (
e.event_detail LIKE @keyword OR
u.username LIKE @keyword OR
u.nickname LIKE @keyword OR
e.event_ip LIKE @keyword
)" : "");
var totalCount = await ExecuteScalarAsync<long>(countSql, parameters);
// 获取分页数据
var events = await ExecuteQueryAsync<UserEventDetail>(sql.ToString(), parameters);
return new PagedResult<UserEventDetail>
{
Total = totalCount,
PageSize = pageSize,
CurrentPage = page,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
Data = events
};
}
}
}

View file

@ -5,5 +5,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Jwt": {
"Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security",
"Issuer": "iFileProxy",
"Audience": "iFileProxy.Client"
}
}

View file

@ -0,0 +1,67 @@
/*
Navicat Premium Data Transfer
Source Server : _阿里云
Source Server Type : MySQL
Source Server Version : 50743
Source Host : 127.0.0.1:3306
Source Schema : iFileProxy
Target Server Type : MySQL
Target Server Version : 50743
File Encoding : 65001
Date: 30/11/2024 19:32:04
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_tasks_info
-- ----------------------------
DROP TABLE IF EXISTS `t_tasks_info`;
CREATE TABLE `t_tasks_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键',
`tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件名',
`client_ip` varchar(46) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP',
`add_time` datetime NOT NULL COMMENT '任务在何时被添加',
`update_time` datetime NOT NULL COMMENT '任务状态更新时间',
`status` int(1) NULL DEFAULT NULL COMMENT '任务状态',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url',
`size` bigint(11) NULL DEFAULT NULL COMMENT '文件大小',
`hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash',
`tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标记',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `tid`(`tid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user_events
-- ----------------------------
DROP TABLE IF EXISTS `t_user_events`;
CREATE TABLE `t_user_events` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`event_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '事件类型',
`trigger_time` datetime NOT NULL COMMENT '触发时间(Server)',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '触发者IP',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_users
-- ----------------------------
DROP TABLE IF EXISTS `t_users`;
CREATE TABLE `t_users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电子邮件',
`role_mask` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '身份掩码',
`nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '外显名称(昵称)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View file

@ -14,6 +14,8 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
</ItemGroup>
</Project>