diff --git a/Apimanager_backend/Apimanager_backend.csproj b/Apimanager_backend/Apimanager_backend.csproj index 533afb7..80575c2 100644 --- a/Apimanager_backend/Apimanager_backend.csproj +++ b/Apimanager_backend/Apimanager_backend.csproj @@ -8,11 +8,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Apimanager_backend/Config/ServiceCollectionExtensions.cs b/Apimanager_backend/Config/ServiceCollectionExtensions.cs index 28a5528..4864289 100644 --- a/Apimanager_backend/Config/ServiceCollectionExtensions.cs +++ b/Apimanager_backend/Config/ServiceCollectionExtensions.cs @@ -1,5 +1,10 @@ using Apimanager_backend.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Text; namespace Apimanager_backend.Config { @@ -8,7 +13,35 @@ namespace Apimanager_backend.Config public static IServiceCollection AddAllService(this IServiceCollection services,IConfiguration configuration) { services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + services.AddJWTService(configuration); services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + public static IServiceCollection AddJWTService(this IServiceCollection services,IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + var key = Encoding.ASCII.GetBytes(jwtSettings["Secret"]); + // JWT配置 + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings["Issuer"], + ValidAudience = jwtSettings["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }; + }); + + //redis配置 + services.AddSingleton(ConnectionMultiplexer.Connect(configuration["Redis:ConnectionString"])); return services; } } diff --git a/Apimanager_backend/Controllers/AuthController.cs b/Apimanager_backend/Controllers/AuthController.cs new file mode 100644 index 0000000..a662105 --- /dev/null +++ b/Apimanager_backend/Controllers/AuthController.cs @@ -0,0 +1,112 @@ +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Models; +using Apimanager_backend.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Apimanager_backend.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class AuthController : ControllerBase + { + private readonly IAuthService authService; + private readonly ITokenService tokenService; + private readonly IRefreshTokenService refreshTokenService; + private readonly IUserService userService; + public AuthController(IAuthService authService, ITokenService tokenService, IRefreshTokenService refreshTokenService,IUserService userService) + { + this.authService = authService; + this.tokenService = tokenService; + this.refreshTokenService = refreshTokenService; + this.userService = userService; + } + /// + /// 用户登录控制器 + /// + /// 登录信息 + /// 通用返回信息格式 + [HttpPost] + public async Task>> Login([FromBody] UserLoginDto dto) + { + try + { + UserInfoDto user = await authService.LoginAsync(dto.UserName, dto.Password); + //生成token + string token = tokenService.GenerateAccessToken(user.Id.ToString(),user.Roles); + //生成refreshtoken + string refreshToken = await refreshTokenService.CreateRefereshTokenAsync(user.Id.ToString()); + var responseInfo = new ResponseBase( + code: 2000, + message: "Login successful", + data: new LoginResponseDto + { + UserInfo = user, + Token = token, + RefreshToken = refreshToken + } + ); + return Ok(responseInfo); + } + catch (BaseException e) + { + + //错误时,构建错误信息对象 + var responseInfo = new ResponseBase( + code: e.code, + message: e.message, + data: null + ); + + return e.code switch + { + 2001 => Unauthorized(responseInfo), + 2002 => Unauthorized(responseInfo), + _ => StatusCode(503) + }; + } + } + + [HttpPost] + public async Task>> Refresh([FromBody]RefreshResponseDto dto) + { + try + { + var userId = await refreshTokenService.ValidateRefreshTokenAsync(dto.RefreshToken); + //刷新令牌无效 + if (userId == null) + { + var ret = new ResponseBase( + code: 2008, + message: "Refresh expires or is invalid", + data: null + ); + return Unauthorized(ret); + } + //获取刷新令牌对应用户信息 + var userInfo = await userService.GetUserAsync(int.Parse(userId)); + //重新生成令牌 + var token = tokenService.GenerateAccessToken(userInfo.Id.ToString(), userInfo.Roles); + //刷新刷新令牌有效期(小于三天才会刷新) + await refreshTokenService.UpdateRefreshTokenAsync(dto.RefreshToken); + var result = new ResponseBase( + code: 1000, + message: "Success", + data: new RefreshResponseDto + { + Token = token, + RefreshToken = dto.RefreshToken + } + + ); + return Ok(result); + }catch(BaseException e) + { + + } + + } + } +} diff --git a/Apimanager_backend/Controllers/UserController.cs b/Apimanager_backend/Controllers/UserController.cs index 279a954..6babc8f 100644 --- a/Apimanager_backend/Controllers/UserController.cs +++ b/Apimanager_backend/Controllers/UserController.cs @@ -7,8 +7,7 @@ using Apimanager_backend.Filters; namespace Apimanager_backend.Controllers { - [ModelValidationFilter()] - [Route("api/[controller]")] + [Route("api/[controller]/[action]")] [ApiController] public class UserController : ControllerBase { @@ -17,37 +16,6 @@ namespace Apimanager_backend.Controllers { this.userService = userService; } - /// - /// 用户登录控制器 - /// - /// 登录信息 - /// 通用返回信息格式 - [HttpPost("Login")] - public async Task>> Login([FromBody]UserLoginDto dto) - { - try - { - UserInfoDto user = await userService.LoginAsync(dto.UserName, dto.Password); - - var responseInfo = new ResponseBase( - code: 2000, - message: "Login successful", - data: user - ); - return Ok(responseInfo); - } - catch (BaseException e) - { - - //错误时,构建错误信息对象 - var responseInfo = new ResponseBase( - code:e.code, - message: e.message, - data: null - ); - - return Unauthorized(responseInfo); - } - } + } } diff --git a/Apimanager_backend/Data/ApiContext.cs b/Apimanager_backend/Data/ApiContext.cs index 9038552..8c86493 100644 --- a/Apimanager_backend/Data/ApiContext.cs +++ b/Apimanager_backend/Data/ApiContext.cs @@ -20,6 +20,8 @@ namespace Apimanager_backend.Data public DbSet Orders { get; set; } //用户已订购套餐表 public DbSet UserPackages { get; set; } + //用户角色表 + public DbSet UserRoles { get; set; } public ApiContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Apimanager_backend/Data/UserRoleConfig.cs b/Apimanager_backend/Data/UserRoleConfig.cs new file mode 100644 index 0000000..401058d --- /dev/null +++ b/Apimanager_backend/Data/UserRoleConfig.cs @@ -0,0 +1,21 @@ +using Apimanager_backend.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Apimanager_backend.Data +{ + public class UserRoleConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + //自增 + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + //外键 + builder.HasOne(x => x.User) + .WithMany(u => u.Roles) + .HasForeignKey(x => x.UserId); + } + } +} diff --git a/Apimanager_backend/Dtos/LoginResponseDto.cs b/Apimanager_backend/Dtos/LoginResponseDto.cs new file mode 100644 index 0000000..0dd5e0d --- /dev/null +++ b/Apimanager_backend/Dtos/LoginResponseDto.cs @@ -0,0 +1,11 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Dtos +{ + public class LoginResponseDto + { + public UserInfoDto UserInfo { get; set; } + public string Token { get; set; } + public string RefreshToken { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/RefreshResponseDto.cs b/Apimanager_backend/Dtos/RefreshResponseDto.cs new file mode 100644 index 0000000..a62a8a0 --- /dev/null +++ b/Apimanager_backend/Dtos/RefreshResponseDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Apimanager_backend.Dtos +{ + public class RefreshResponseDto + { + public string? Token { get; set; } + [Required(ErrorMessage = "RefreshToken is required")] + public string RefreshToken { get; set; } + } +} diff --git a/Apimanager_backend/Dtos/UserInfoBaseDto.cs b/Apimanager_backend/Dtos/UserInfoBaseDto.cs new file mode 100644 index 0000000..485c1b2 --- /dev/null +++ b/Apimanager_backend/Dtos/UserInfoBaseDto.cs @@ -0,0 +1,16 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Dtos +{ + public class UserInfoBaseDto + { + public UserInfoDto UserInfo { get; set; } + public List Roles { get; set; } + public UserInfoBaseDto(UserInfoDto userInfo, List roles) + { + UserInfo = userInfo; + Roles = roles; + } + public UserInfoBaseDto() { } + } +} diff --git a/Apimanager_backend/Dtos/UserInfoDto.cs b/Apimanager_backend/Dtos/UserInfoDto.cs index 927ffed..afe3cca 100644 --- a/Apimanager_backend/Dtos/UserInfoDto.cs +++ b/Apimanager_backend/Dtos/UserInfoDto.cs @@ -7,7 +7,7 @@ namespace Apimanager_backend.Dtos public int Id { get; set; } public string UserName { get; set; } public string Email { get; set; } - public UserRole Role { get; set; } + public List Roles { get; set; } public bool IsBan { get; set; } public decimal Balance { get; set; } public DateTime Created { get; set; } diff --git a/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs new file mode 100644 index 0000000..d4a7a0f --- /dev/null +++ b/Apimanager_backend/Filters/ExceptionFilter/generalExceptionFilter.cs @@ -0,0 +1,42 @@ +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Apimanager_backend.Filters.ExceptionFilter +{ + public class generalExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + if(context.Exception is BaseException) + { + //构造通用错误返回结果 + BaseException exception = (BaseException)context.Exception; + var res = new ResponseBase( + code:exception.code, + message: exception.message, + data:null + ); + //根据自定义错误码返回对应http状态码 + int code = 0; + switch (exception.code) + { + case 1001: + case 1005: + case 2007: + case 4001: + code = 400; + break; + case 1002: + case 2001: + code = 401; + break; + case 1003: + case 2006: + case 3001: + + } + } + } + } +} diff --git a/Apimanager_backend/Filters/ModelValidationFilter.cs b/Apimanager_backend/Filters/ModelValidationFilter.cs index 8349db8..7760d2e 100644 --- a/Apimanager_backend/Filters/ModelValidationFilter.cs +++ b/Apimanager_backend/Filters/ModelValidationFilter.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Apimanager_backend.Filters { - public class ModelValidationFilter : Attribute,IActionFilter + public class ModelValidationFilter :IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { } diff --git a/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs new file mode 100644 index 0000000..1fb1294 --- /dev/null +++ b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.Designer.cs @@ -0,0 +1,408 @@ +// +using System; +using Apimanager_backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + [DbContext(typeof(ApiContext))] + [Migration("20241030160723_add-userrole-table")] + partial class adduserroletable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("IsThirdParty") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.ToTable("Apis"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiId") + .HasColumnType("int"); + + b.Property("CallResult") + .HasColumnType("int"); + + b.Property("CallTime") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApiId"); + + b.HasIndex("UserId"); + + b.ToTable("CallLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CallLimit") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Apipackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("varchar(45)"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TargetId") + .HasColumnType("int"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("OperationLogs"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OrderType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThirdPartyOrderId") + .HasColumnType("varchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("ThirdPartyOrderId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Balance") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsBan") + .HasColumnType("tinyint(1)"); + + b.Property("IsDelete") + .HasColumnType("tinyint(1)"); + + b.Property("PassHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("PurchasedAt") + .HasColumnType("datetime(6)"); + + b.Property("RemainingCalls") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPackages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Apis") + .HasForeignKey("PackageId"); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.ApiCallLog", b => + { + b.HasOne("Apimanager_backend.Models.Api", "Api") + .WithMany("ApiCalls") + .HasForeignKey("ApiId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("CallLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Api"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.OperationLog", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("operationLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Order", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserPackage", b => + { + b.HasOne("Apimanager_backend.Models.Apipackage", "Package") + .WithMany("Packages") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Packages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Api", b => + { + b.Navigation("ApiCalls"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.Apipackage", b => + { + b.Navigation("Apis"); + + b.Navigation("Packages"); + }); + + modelBuilder.Entity("Apimanager_backend.Models.User", b => + { + b.Navigation("CallLogs"); + + b.Navigation("Orders"); + + b.Navigation("Packages"); + + b.Navigation("Roles"); + + b.Navigation("operationLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs new file mode 100644 index 0000000..f57483e --- /dev/null +++ b/Apimanager_backend/Migrations/20241030160723_add-userrole-table.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Apimanager_backend.Migrations +{ + /// + public partial class adduserroletable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Role", + table: "Users"); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + Role = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => x.Id); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_UserId", + table: "UserRoles", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.AddColumn( + name: "Role", + table: "Users", + type: "int", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs index 83d8249..1d156eb 100644 --- a/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs +++ b/Apimanager_backend/Migrations/ApiContextModelSnapshot.cs @@ -233,9 +233,6 @@ namespace Apimanager_backend.Migrations .HasMaxLength(255) .HasColumnType("varchar(255)"); - b.Property("Role") - .HasColumnType("int"); - b.Property("Username") .IsRequired() .HasColumnType("varchar(255)"); @@ -278,6 +275,26 @@ namespace Apimanager_backend.Migrations b.ToTable("UserPackages"); }); + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRoles"); + }); + modelBuilder.Entity("Apimanager_backend.Models.Api", b => { b.HasOne("Apimanager_backend.Models.Apipackage", "Package") @@ -347,6 +364,17 @@ namespace Apimanager_backend.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Apimanager_backend.Models.UserRole", b => + { + b.HasOne("Apimanager_backend.Models.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Apimanager_backend.Models.Api", b => { b.Navigation("ApiCalls"); @@ -367,6 +395,8 @@ namespace Apimanager_backend.Migrations b.Navigation("Packages"); + b.Navigation("Roles"); + b.Navigation("operationLogs"); }); #pragma warning restore 612, 618 diff --git a/Apimanager_backend/Models/User.cs b/Apimanager_backend/Models/User.cs index 07c11fc..c3c3eb1 100644 --- a/Apimanager_backend/Models/User.cs +++ b/Apimanager_backend/Models/User.cs @@ -30,7 +30,7 @@ namespace Apimanager_backend.Models /// /// 用户角色 /// - public UserRole Role { get; set; } // Enum('Admin','User') + //public UserRole Role { get; set; } // Enum('Admin','User') /// /// 是否禁用 @@ -56,5 +56,6 @@ namespace Apimanager_backend.Models public ICollection operationLogs { get; set; } public ICollection CallLogs { get; set; } public ICollection Orders { get; set; } + public ICollection Roles { get; set; } = new List(); } } diff --git a/Apimanager_backend/Models/UserRole.cs b/Apimanager_backend/Models/UserRole.cs index cbd29ff..1df0b7a 100644 --- a/Apimanager_backend/Models/UserRole.cs +++ b/Apimanager_backend/Models/UserRole.cs @@ -1,8 +1,15 @@ -namespace Apimanager_backend.Models +using System.Text.Json.Serialization; + +namespace Apimanager_backend.Models { - public enum UserRole + public class UserRole { - Admin = 0, - User = 1 + public int Id { get; set; } + public int UserId { get; set; } + public string Role { get; set; } + + //导航属性 + [JsonIgnore] + public User User { get; set; } } } diff --git a/Apimanager_backend/Program.cs b/Apimanager_backend/Program.cs index 78c9887..7c8df94 100644 --- a/Apimanager_backend/Program.cs +++ b/Apimanager_backend/Program.cs @@ -21,7 +21,15 @@ builder.Services.AddAllService(configuration); builder.Services.AddControllers(options => { options.Filters.Add(); +}).ConfigureApiBehaviorOptions(option => +{ + option.SuppressModelStateInvalidFilter = true; +}) +.AddJsonOptions(options => +{ + options.JsonSerializerOptions.MaxDepth = 64; }); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -37,6 +45,7 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/Apimanager_backend/Services/AuthService.cs b/Apimanager_backend/Services/AuthService.cs new file mode 100644 index 0000000..156309d --- /dev/null +++ b/Apimanager_backend/Services/AuthService.cs @@ -0,0 +1,41 @@ +using Apimanager_backend.Data; +using Apimanager_backend.Dtos; +using Apimanager_backend.Exceptions; +using Apimanager_backend.Models; +using AutoMapper; +using Microsoft.EntityFrameworkCore; + +namespace Apimanager_backend.Services +{ + public class AuthService:IAuthService + { + private readonly ApiContext apiContext; + private readonly IMapper mapper; + public AuthService(ApiContext apiContext, IMapper automapper) + { + this.apiContext = apiContext; + this.mapper = automapper; + } + public async Task LoginAsync(string username, string password) + { + //查找用户 + User? user = await apiContext.Users.Include(x => x.Roles).SingleOrDefaultAsync(x => + x.Username == username && x.PassHash == password + ); + + //用户不存在或密码错误都为登录失败 + if (user == null) + { + throw new BaseException(2001, "Invalid username or password"); + } + + //用户被禁用 + if (user.IsBan) + { + throw new BaseException(2002, "User account is disabled"); + } + + return mapper.Map(user); + } + } +} diff --git a/Apimanager_backend/Services/IAuthService.cs b/Apimanager_backend/Services/IAuthService.cs new file mode 100644 index 0000000..17c035c --- /dev/null +++ b/Apimanager_backend/Services/IAuthService.cs @@ -0,0 +1,15 @@ +using Apimanager_backend.Dtos; + +namespace Apimanager_backend.Services +{ + public interface IAuthService + { + /// + /// 登录用户,根据用户名和密码进行身份验证。 + /// + /// 用户名 + /// 密码 + /// 包含用户信息的 + Task LoginAsync(string username, string password); + } +} diff --git a/Apimanager_backend/Services/IRefreshTokenService.cs b/Apimanager_backend/Services/IRefreshTokenService.cs new file mode 100644 index 0000000..343c434 --- /dev/null +++ b/Apimanager_backend/Services/IRefreshTokenService.cs @@ -0,0 +1,30 @@ +namespace Apimanager_backend.Services +{ + public interface IRefreshTokenService + { + /// + /// 创建刷新令牌 + /// + /// 用户id + /// 刷新令牌 + Task CreateRefereshTokenAsync(string userId); + /// + /// 验证刷新令牌 + /// + /// 刷新令牌 + /// 是否验证通过 + Task ValidateRefreshTokenAsync(string refreshToken); + /// + /// 删除刷新令牌 + /// + /// 刷新令牌 + /// 是否删除成功 + Task DeleterRefreshTokenAsync(string refreshToken); + /// + /// 更新刷新令牌有效期 + /// + /// 刷新令牌 + /// 是否成功 + Task UpdateRefreshTokenAsync(string refreshToken); + } +} diff --git a/Apimanager_backend/Services/ITokenService.cs b/Apimanager_backend/Services/ITokenService.cs new file mode 100644 index 0000000..1016997 --- /dev/null +++ b/Apimanager_backend/Services/ITokenService.cs @@ -0,0 +1,16 @@ +using Apimanager_backend.Models; + +namespace Apimanager_backend.Services +{ + public interface ITokenService + { + /// + /// 拥护凭证 + /// + /// 用户ID + /// 用户名 + /// 角色 + /// token + string GenerateAccessToken(string userId, List roles); + } +} diff --git a/Apimanager_backend/Services/IUserService.cs b/Apimanager_backend/Services/IUserService.cs index 0d2212a..1162650 100644 --- a/Apimanager_backend/Services/IUserService.cs +++ b/Apimanager_backend/Services/IUserService.cs @@ -6,13 +6,7 @@ namespace Apimanager_backend.Services { public interface IUserService { - /// - /// 登录用户,根据用户名和密码进行身份验证。 - /// - /// 用户名 - /// 密码 - /// 包含用户信息的 - Task LoginAsync(string username, string password); + /// /// 发送密码重置邮件到指定邮箱。 @@ -33,9 +27,9 @@ namespace Apimanager_backend.Services /// /// 获取用户信息。 /// - /// 用户名 + /// 用户ID /// 包含用户信息的 - Task GetUserAsync(string username); + Task GetUserAsync(int userId); /// /// 更新用户信息。 diff --git a/Apimanager_backend/Services/RefreshTokenService.cs b/Apimanager_backend/Services/RefreshTokenService.cs new file mode 100644 index 0000000..5fc458e --- /dev/null +++ b/Apimanager_backend/Services/RefreshTokenService.cs @@ -0,0 +1,68 @@ +using Apimanager_backend.Exceptions; +using StackExchange.Redis; + +namespace Apimanager_backend.Services +{ + public class RefreshTokenService : IRefreshTokenService + { + private readonly IConnectionMultiplexer redis; + private readonly IConfiguration configuration; + public RefreshTokenService(IConnectionMultiplexer redis, IConfiguration configuration) + { + this.redis = redis; + this.configuration = configuration; + } + + public async Task CreateRefereshTokenAsync(string userId) + { + var refreshToken = Guid.NewGuid().ToString(); + var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); + + // 保存到Redis,设置过期时间 + var db = redis.GetDatabase(); + var res = await db.StringSetAsync(refreshToken, userId, TimeSpan.FromDays(expiryDays)); + if (!res) + { + throw new BaseException(1006, "Service unavailable"); + } + return refreshToken; + } + + public async Task DeleterRefreshTokenAsync(string refreshToken) + { + var db = redis.GetDatabase(); + bool res = await db.KeyDeleteAsync(refreshToken); + if (!res) + { + throw new BaseException(1006, "Service unavailable"); + } + } + + public async Task UpdateRefreshTokenAsync(string refreshToken) + { + var db = redis.GetDatabase(); + var expiryDays = Convert.ToDouble(configuration["JwtSettings:RefreshTokenExpiryDays"]); + //获取refresh剩余有效时间 + var time =await db.KeyTimeToLiveAsync(refreshToken); + //判断有效时间是否大于零天小于三天,否则不刷新有效期 + if(time <= TimeSpan.Zero || time >= TimeSpan.FromDays(3)) + { + return; + } + //刷新过期时间 + await db.KeyExpireAsync(refreshToken,TimeSpan.FromDays(expiryDays)); + } + + public async Task ValidateRefreshTokenAsync(string refreshToken) + { + var db = redis.GetDatabase(); + var redisValue = await db.StringGetAsync(refreshToken); + //验证refreshToken是否存在 + if (!redisValue.HasValue) + { + return null; + } + return redisValue.ToString(); + } + } +} diff --git a/Apimanager_backend/Services/TokenService.cs b/Apimanager_backend/Services/TokenService.cs new file mode 100644 index 0000000..28215a5 --- /dev/null +++ b/Apimanager_backend/Services/TokenService.cs @@ -0,0 +1,50 @@ + +using Apimanager_backend.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Apimanager_backend.Services +{ + public class TokenService:ITokenService + { + public readonly IConfiguration configuration; + public TokenService(IConfiguration configuration) + { + this.configuration = configuration; + } + + public string GenerateAccessToken(string userId,List roles) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + + // 创建Claims列表,包含用户名和角色信息 + var claims = new List + { + new Claim("userId", userId), // 使用userId作为唯一标识 + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + //添加用户角色 + foreach(var role in roles) + { + var claim = new Claim(ClaimTypes.Role, role.Role.ToString()); + claims.Add(claim); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Secret"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: jwtSettings["Issuer"], + audience: jwtSettings["Audience"], + claims: claims, + expires: DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["AccessTokenExpiryMinutes"])), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} diff --git a/Apimanager_backend/Services/UserService.cs b/Apimanager_backend/Services/UserService.cs index c6859a7..35fe2e2 100644 --- a/Apimanager_backend/Services/UserService.cs +++ b/Apimanager_backend/Services/UserService.cs @@ -34,9 +34,15 @@ namespace Apimanager_backend.Services throw new NotImplementedException(); } - public Task GetUserAsync(string username) + public async Task GetUserAsync(int userId) { - throw new NotImplementedException(); + User? user = await apiContext.Users.SingleOrDefaultAsync(x => x.Id == userId); + //未找到用户 + if (user == null) + { + throw new BaseException(2004, "User not found"); + } + return mapper.Map(user); } public Task> GetUsersAsync(int page, int pageSize, bool desc) @@ -44,27 +50,6 @@ namespace Apimanager_backend.Services throw new NotImplementedException(); } - public async Task LoginAsync(string username, string password) - { - //查找用户 - User? user = await apiContext.Users.SingleOrDefaultAsync(x => - x.Username == username && x.PassHash == password - ); - - //用户不存在或密码错误都为登录失败 - if(user == null) - { - throw new BaseException(2001, "Invalid username or password"); - } - - //用户被禁用 - if (user.IsBan) - { - throw new BaseException(2002, "User account is disabled"); - } - - return mapper.Map(user); - } public Task ResetPasswordAsync(string email, string token, string newPassword) { diff --git a/Apimanager_backend/appsettings.json b/Apimanager_backend/appsettings.json index 8fbdf86..fb79bdb 100644 --- a/Apimanager_backend/appsettings.json +++ b/Apimanager_backend/appsettings.json @@ -8,5 +8,15 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "server=192.168.5.200;username=root;password=768788Dyw@;port=3306;database=api_billing_system;SslMode=Preferred;" + }, + "JwtSettings": { + "Secret": "deXtdXode6hv0SI1o6xRw1ALkn0vYsWn", + "Issuer": "qinglan", + "Audience": "qinglan", + "AccessTokenExpiryMinutes": 60, + "RefreshTokenExpiryDays": 7 + }, + "redis": { + "ConnectionString": "192.168.5.200:6379" } } diff --git a/ErrorCode.md b/ErrorCode.md index 9c36c73..7b5d551 100644 --- a/ErrorCode.md +++ b/ErrorCode.md @@ -14,16 +14,18 @@ #### 用户模块错误码(2xxx) -| 错误码 | HTTP状态码 | 描述 | Message | -| ------ | ---------- | -------------------- | ---------------------------- | -| 2000 | 200 | 登录成功 | Login successful | -| 2001 | 401 | 用户名或密码错误 | Invalid username or password | -| 2002 | 401 | 用户账户被禁用 | User account is disabled | -| 2003 | 409 | 用户名已存在 | Username already exists | -| 2004 | 404 | 用户不存在 | User not found | -| 2005 | 409 | 邮箱已存在 | Email already exists | -| 2006 | 403 | 用户无权限进行该操作 | Permission denied | -| 2007 | 400 | 密码重置失败 | Password reset failed | +| 错误码 | HTTP状态码 | 描述 | Message | +| ------ | ---------- | -------------------- | ----------------------------- | +| 2000 | 200 | 登录成功 | Login successful | +| 2001 | 401 | 用户名或密码错误 | Invalid username or password | +| 2002 | 401 | 用户账户被禁用 | User account is disabled | +| 2003 | 409 | 用户名已存在 | Username already exists | +| 2004 | 404 | 用户不存在 | User not found | +| 2005 | 409 | 邮箱已存在 | Email already exists | +| 2006 | 403 | 用户无权限进行该操作 | Permission denied | +| 2007 | 400 | 密码重置失败 | Password reset failed | +| 2008 | 403 | 凭证到期或无效 | Token expires or is invalid | +| 2009 | 403 | 刷新令牌到期或无效 | Refresh expires or is invalid | #### API模块错误码(3xxx)