日志25-7-17

本文最后更新于 2025年7月24日星期四 17:34

今天学习完EF Core,开始做DEMO【图书管理系统】

注解校验

ModelState.IsValid:判断前端传来的字段是否符合实体类上的注解约束声明

自动更新UpdatedTime字段

自定义审计接口

1
2
3
4
public class IAuditableEntity
{
public DateTime UpdatedTime { get; set; }
}

实体类实现接口

1
2
3
4
public class Author : IAuditableEntity
{
// ...
}

在DbContext中重写SaveChanges()和SaveChangesAsync()

1
2
3
4
5
6
7
8
9
10
public override int SaveChanges()
{
ApplyAuditInformation(); // 调用私有方法来更新 UpdatedTime
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyAuditInformation(); // 调用私有方法来更新 UpdatedTime
return await base.SaveChangesAsync(cancellationToken);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void ApplyAuditInformation()
{
// 获取所有处于 Added (新增) 或 Modified (修改) 状态的实体条目
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is IAuditableEntity auditableEntity) // 检查实体是否实现了 IAuditableEntity 接口
{
if (entry.State == EntityState.Modified)
{
// 如果实体被修改,更新 UpdatedTime
auditableEntity.UpdatedTime = DateTime.Now;

// 可选:如果你想确保 CreatedTime 不被修改,可以在这里处理
// entry.Property(nameof(IAuditableEntity.CreatedTime)).IsModified = false;
}
// 你也可以在这里处理 EntityState.Added 状态的实体,如果它们的 CreatedTime 没有在构造函数中设置
// else if (entry.State == EntityState.Added)
// {
// auditableEntity.CreatedTime = DateTime.Now;
// auditableEntity.UpdatedTime = DateTime.Now;
// }
}
}
}

生成基础CRUD

联结实体类

书 和 书的类别 是多对多的关系:
BookCategory:

1
2
3
4
5
6
7
8
public class BookCategory
{
public int BookId { get; set; }
public Book Book { get; set; }

public int CategoryId { get; set; }
public Category Category { get; set; }
}

ApplicationDbContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// 在此进行模型设定...
// 1. 定义复合主键
modelBuilder.Entity<BookCategory>()
.HasKey(bc => new { bc.BookId, bc.CategoryId });

// 2. 设定从 BookCategory 到 Book 的关系
// 一个BookCategory 只能对应一个 Book
// 一个Book 可以对应多个 BookCategory 记录
modelBuilder.Entity<BookCategory>()
.HasOne(bc => bc.Book)
.WithMany(b => b.BookCategories)
.HasForeignKey(bc => bc.BookId);

// 3. 设定从 BookCategory 到 Category 的关系
// 一个BookCategory 只能对应一个 Category
// 一个Category 可以对应多个 BookCategory 记录
modelBuilder.Entity<BookCategory>()
.HasOne(bc => bc.Category)
.WithMany(c => c.BookCategories)
.HasForeignKey(bc => bc.CategoryId);
}

JWT

使用步骤

  1. 安装2个NuGet包
    1
    2
    dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
    dotnet add package System.IdentityModel.Tokens.Jwt
  2. appsettings.json中配置 JWT
    1
    2
    3
    4
    5
    6
    "JwtSettings": {
    "_comment": "SecretKey:令牌的颁发者(通常是你的API域名);Issuer:令牌的接收者(通常是你的前端域名或客户端应用标识)",
    "SecretKey": "这是一个非常非常非常非常非常安全的秘密密钥!长度必须足够长且复杂,例如至少32个随机字符。",
    "Issuer": "YourApiIssuer",
    "Audience": "YourApiClient"
    }
  3. Program.cs中添加认证支持
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // JWT配置开始
    IConfigurationSection jwtSettings = builder.Configuration.GetSection("JwtSettings");
    string secretKey = jwtSettings["SecretKey"]; // JWT签名密钥
    string issuer = jwtSettings["Issuer"]; // JWT令牌发布者
    string audience = jwtSettings["Audience"]; // JWT令牌接收者
    if (string.IsNullOrEmpty(secretKey))
    {
    throw new InvalidOperationException("JWT 密钥没有配置好");
    }

    byte[] key = Encoding.ASCII.GetBytes(secretKey); // 将密钥字符串转为字节数组
    builder.Services.AddAuthentication(options =>
    {
    // 设置默认的认证方案和挑战方案为JWT Bearer
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
    // 在开发环境可以设置为false,生产环境必须设置为true,强制HTTPS
    options.RequireHttpsMetadata = false;
    options.SaveToken = true; // 是否在HttpContext中存储JWT
    options.TokenValidationParameters = new TokenValidationParameters()
    {
    ValidateIssuerSigningKey = true, // 验证签名密钥,确保令牌没有被篡改
    IssuerSigningKey = new SymmetricSecurityKey(key), // 用于签名的密钥
    ValidateIssuer = true, // 验证令牌的发布者
    ValidateAudience = true, // 验证令牌的接收者
    ValidateLifetime = true, // 验证令牌有效期(过期时间)
    ValidIssuer = issuer, // 有效的发布者
    ValidAudience = audience, // 有效的接收者
    ClockSkew = TimeSpan.Zero // 令牌有效期时钟偏移量,默认为5分钟,这里设置为0,表示令牌有效期必须与当前时间一致
    };

    });
    builder.Services.AddAuthentication(); // 启用授权服务
    // JWT配置结束
  4. 创建JWT Token生成的方法,修改登录逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    private (string Token, DateTime ExpiresAt) GenerateJwtToken(User user)
    {
    var jwtSettings = _configuration.GetSection("JwtSettings");
    var secretKey = jwtSettings["SecretKey"]!;
    var issuer = jwtSettings["Issuer"]!;
    var audience = jwtSettings["Audience"]!;

    var key = Encoding.ASCII.GetBytes(secretKey);

    // JWT 声明 (Claims):这些信息将包含在令牌中
    var claims = new[]
    {
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), // 用户唯一标识符
    new Claim(ClaimTypes.Name, user.Name), // 用户名
    new Claim(ClaimTypes.Email, user.Email), // 邮箱
    // 可以根据需要添加其他自定义声明,如用户角色
    new Claim(ClaimTypes.Role, nameof(user.Role)), // 假设 User 实体有 Role 属性
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // JWT 的唯一 ID,用于黑名单(可选)
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var expires = DateTime.UtcNow.AddHours(1); // 令牌有效期为 1 小时,可以根据需求调整

    var tokenDescriptor = new SecurityTokenDescriptor
    {
    Subject = new ClaimsIdentity(claims),
    Expires = expires,
    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
    Issuer = issuer,
    Audience = audience
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);

    return (tokenHandler.WriteToken(token), expires);
    }
  5. 使用注解保护API
    [Authorize]:必须通过JWT的考验
    [AllowAnonymous]:允许匿名访问
  6. 客户端如何存储和发送JWT
    TOKEN存储在本地,每次发请求的时候把TOKEN放在HTTP头的authorization里:Bearer 这里写TOKEN串

日志25-7-17
https://zhiyun.blog/日志25-7-17/
作者
Okita
发布于
2025年7月17日
许可协议