برنامه نویسی

چگونه می توان هویت اصلی ASP.NET را با EF Core برای نیازهای پروژه خود تنظیم کرد

امنیت و احراز هویت یکی از مهمترین جنبه های هر برنامه است.
درک و استفاده از ابزارهای اثبات شده برای جلوگیری از آسیب پذیری های مشترک مانند دسترسی غیرمجاز و نشت داده ها بسیار مهم است.

هویت اصلی Asp.net به توسعه دهندگان روشی قدرتمند برای مدیریت کاربران ، نقش ها ، ادعاها و انجام احراز هویت کاربر برای برنامه های وب ارائه می دهد.
هویت راه حل های آماده برای استفاده و API را از جعبه فراهم می کند.
اما اغلب شما باید تنظیماتی را متناسب با نیازها یا نیازهای خاص خود انجام دهید.

امروز می خواهم رویکردهای عملی را برای شخصی سازی هویت ASP.NET به مرحله به مرحله نشان دهم.

ما کاوش خواهیم کرد:

  • نحوه تطبیق جداول هویت داخلی با طرح پایگاه داده خود
  • نحوه ثبت و ورود کاربران با نشانه های JWT
  • نحوه به روزرسانی نقش ها و ادعاهای کاربر با هویت
  • نحوه بذر نقش ها و ادعاهای اولیه با استفاده از هویت.

بیایید شیرجه بزنیم!

در وب سایت من: antondevtips.com من بهترین روش های .NET و معماری را به اشتراک می گذارم.
برای بهبود مهارت های دات نت خود در خبرنامه من مشترک شوید.
کد منبع را برای این خبرنامه به صورت رایگان بارگیری کنید.

شروع با هویت ASP.NET

هویت اصلی Asp.net مجموعه ای از ابزارهایی است که عملکرد ورود به سیستم را به برنامه های اصلی ASP.NET اضافه می کند.
این وظایف مانند ایجاد کاربران جدید ، عبور رمزهای عبور ، اعتبار اعتبارنامه کاربر و مدیریت نقش ها یا ادعاها را انجام می دهد.

بسته های زیر را به پروژه خود اضافه کنید تا با هویت اصلی ASP.NET شروع به کار کنید:

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا نحوه پیکربندی هویت پیش فرض آمده است:

builder.Services.AddDefaultIdentity<IdentityUser>(options => {})
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Password settings.
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;

    // Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // User settings.
    options.User.AllowedUserNameCharacters =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = false;
});
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا لیست موجودات موجود ارائه شده توسط هویت وجود دارد:

  • کاربر
  • نقش
  • ادعا کردن
  • ادعای کاربر
  • ارباب
  • باسله
  • متمایز
  • کارگردان

با این حال ، در بسیاری از برنامه ها ممکن است نیاز به ایجاد موارد زیر داشته باشید:

  • زمینه های سفارشی خود را به User موجودیت
  • زمینه های سفارشی خود را به Role موجودیت
  • کاربر و نقش کودک در هنگام استفاده مرجع IdentityDbContext
  • از نشانه های JWT به جای نشانه های تحمل استفاده کنید (بله ، هویت هسته ASP.NET هنگام استفاده از API های وب آماده ، نشانه های تحمل را برمی گرداند)

بیایید بررسی کنیم که چگونه می توانید طرح پایگاه داده را برای هویت با EF Core سفارشی کنید.

سفارشی سازی هویت ASP.NET با EF Core

یکی از ویژگی های عالی هویت اصلی ASP.NET این است که می توانید اشخاص و جداول پایگاه داده مربوطه را سفارشی کنید.

شما می توانید کلاس خود را ایجاد کنید و از IdentityUser برای افزودن زمینه های جدید (نام FullName و JobTitle):

public class User : IdentityUser
{
    public string FullName { get; set; }

    public string JobTitle { get; set; }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بعد باید از IdentityDbContext و نوع کلاس کاربر سفارشی خود را مشخص کنید:

public class BooksDbContext : IdentityDbContext<User>
{
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

شما می توانید به طور معمول یک نهاد کاربر را در سایر نهادها ارجاع دهید:

public class Author
{
    public required Guid Id { get; set; }

    public required string Name { get; set; }

    public List<Book> Books { get; set; } = [];

    public string? UserId { get; set; }

    public User? User { get; set; }
}

public class AuthorConfiguration : IEntityTypeConfiguration<Author>
{
    public void Configure(EntityTypeBuilder<Author> builder)
    {
        builder.ToTable("authors");
        builder.HasKey(x => x.Id);

        // ...

        builder.HasOne(x => x.User)
            .WithOne()
            .HasForeignKey<Author>(x => x.UserId);
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بیایید گسترش دهیم User نهاد بیشتر برای اینکه بتوانید به ادعاهای کاربر ، نقش ها ، ورود به سیستم و نشانه ها بروید:

public class User : IdentityUser
{
    public ICollection<UserClaim> Claims { get; set; }

    public ICollection<UserRole> UserRoles { get; set; }

    public ICollection<UserLogin> UserLogins { get; set; }

    public ICollection<UserToken> UserTokens { get; set; }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ما باید نقشه برداری را برای User موجودی برای مشخص کردن روابط:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        // Each User can have many UserClaims
        builder.HasMany(e => e.Claims)
            .WithOne(e => e.User)
            .HasForeignKey(uc => uc.UserId)
            .IsRequired();

        // Each User can have many UserLogins
        builder.HasMany(e => e.UserLogins)
            .WithOne(e => e.User)
            .HasForeignKey(ul => ul.UserId)
            .IsRequired();

        // Each User can have many UserTokens
        builder.HasMany(e => e.UserTokens)
            .WithOne(e => e.User)
            .HasForeignKey(ut => ut.UserId)
            .IsRequired();

        // Each User can have many entries in the UserRole join table
        builder.HasMany(e => e.UserRoles)
            .WithOne(e => e.User)
            .HasForeignKey(ur => ur.UserId)
            .IsRequired();
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اگر می خواهید تمام روابط بین نهادهای هویت را داشته باشید ، باید کلاس های دیگری را نیز گسترش دهید:

public class Role : IdentityRole
{
    public ICollection<UserRole> UserRoles { get; set; }
    public ICollection<RoleClaim> RoleClaims { get; set; }
}

public class RoleClaim : IdentityRoleClaim<string>
{
    public Role Role { get; set; }
}

public class UserRole : IdentityUserRole<string>
{
    public User User { get; set; }
    public Role Role { get; set; }
}

public class UserClaim : IdentityUserClaim<string>
{
    public User User { get; set; }
}

public class UserLogin : IdentityUserLogin<string>
{
    public User User { get; set; }
}

public class UserToken : IdentityUserToken<string>
{
    public User User { get; set; }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا نقشه برداری برای اشخاص است:

public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
    public void Configure(EntityTypeBuilder<Role> builder)
    {
        builder.ToTable("roles");

        // Each Role can have many entries in the UserRole join table
        builder.HasMany(e => e.UserRoles)
            .WithOne(e => e.Role)
            .HasForeignKey(ur => ur.RoleId)
            .IsRequired();

        // Each Role can have many associated RoleClaims
        builder.HasMany(e => e.RoleClaims)
            .WithOne(e => e.Role)
            .HasForeignKey(rc => rc.RoleId)
            .IsRequired();
    }
}

public class RoleClaimConfiguration : IEntityTypeConfiguration<RoleClaim>
{
    public void Configure(EntityTypeBuilder<RoleClaim> builder)
    {
        builder.ToTable("role_claims");
    }
}

public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
{
    public void Configure(EntityTypeBuilder<UserRole> builder)
    {
        builder.HasKey(x => new { x.UserId, x.RoleId });
    }
}

public class UserClaimConfiguration : IEntityTypeConfiguration<UserClaim>
{
    public void Configure(EntityTypeBuilder<UserClaim> builder)
    {
        builder.ToTable("user_claims");
    }
}

public class UserLoginConfiguration : IEntityTypeConfiguration<UserLogin>
{
    public void Configure(EntityTypeBuilder<UserLogin> builder)
    {
        builder.ToTable("user_logins");
    }
}

public class UserTokenConfiguration : IEntityTypeConfiguration<UserToken>
{
    public void Configure(EntityTypeBuilder<UserToken> builder)
    {
        builder.ToTable("user_tokens");
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ممکن است خیلی به نظر برسد ، اما شما باید یک بار این کار را انجام دهید و سپس در هر پروژه از آن استفاده کنید.

PS: می توانید کد منبع کامل را در انتهای مقاله بارگیری کنید.

در اینجا آمده است که چگونه BooksDbContext تغییرات:

public class BooksDbContext : IdentityDbContext<User, Role, string,
    UserClaim, UserRole, UserLogin,
    RoleClaim, UserToken>
{
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

سرانجام ، شما باید هویت را در DI ثبت کنید:

services.AddDbContext<BooksDbContext>((provider, options) =>
{
    options
        .UseNpgsql(connectionString, npgsqlOptions =>
        {
            npgsqlOptions.MigrationsHistoryTable(DatabaseConsts.MigrationTableName,
                DatabaseConsts.Schema);
        })
        .UseSnakeCaseNamingConvention();
});

services
    .AddIdentity<User, Role>(options =>
    {
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequiredLength = 8;
    })
    .AddEntityFrameworkStores<BooksDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

به طور پیش فرض ، تمام جداول و ستون های موجود در پایگاه داده در Pascalcase قرار می گیرند.
اگر الگوهای نامگذاری سازگار و اتوماتیک را ترجیح می دهید ، بسته را در نظر بگیرید EFCore.NamingConventions:

dotnet add package EFCore.NamingConventions
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از نصب ، شما به سادگی باید پشتیبانی از کنوانسیون نامگذاری را به پیکربندی DBContext خود وصل کنید.
در ثبت نام DBContext من در بالا استفاده کردم UseSnakeCaseNamingConvention() برای پایگاه داده Postgres من.
بنابراین نام جدول و ستون به نظر می رسد: user_claimsبا user_id، و غیره

ثبت کاربران با هویت

اکنون که هویت سیم کشی شده است ، می توانید یک نقطه پایانی را در معرض نمایش بگذارید تا کاربران جدید ثبت نام کنند.
در اینجا یک مثال حداقل API آورده شده است:

public record RegisterUserRequest(string Email, string Password);

app.MapPost("/api/register", async (
    [FromBody] RegisterUserRequest request,
    UserManager<User> userManager) =>
{
    var existingUser = await userManager.FindByEmailAsync(request.Email);
    if (existingUser != null)
    {
        return Results.BadRequest("User already exists.");
    }

    var user = new User
    {
        UserName = request.Email,
        Email = request.Email
    };

    var result = await userManager.CreateAsync(user, request.Password);
    if (!result.Succeeded)
    {
        return Results.BadRequest(result.Errors);
    }

    result = await userManager.AddToRoleAsync(user, "DefaultRole");

    if (!result.Succeeded)
    {
        return Results.BadRequest(result.Errors);
    }

    var response = new UserResponse(user.Id, user.Email);
    return Results.Created($"/api/users/{user.Id}", response);
});
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

شما می توانید از UserManager کلاس برای مدیریت کاربران.

ثبت نام شامل مراحل زیر است:

  1. شما باید بررسی کنید که آیا کاربر از قبل با تماس وجود دارد FindByEmailAsync روش
  2. اگر کاربر وجود نداشته باشد ، می توانید با تماس با یک کاربر جدید ایجاد کنید CreateAsync روش
  3. با فراخوانی می توانید کاربر را به نقش اضافه کنید AddToRoleAsync روش
  4. در صورت خطا در هنگام ثبت نام – می توانید a را برگردانید BadRequest پاسخ با پیام خطا از هویت.

نحوه ورود کاربران با هویت

پس از ایجاد کاربر ، آنها می توانند تأیید اعتبار کنند.
در اینجا نحوه اجرای احراز هویت با استفاده از هویت آمده است:

using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

public record LoginUserRequest(string Email, string Password);

app.MapPost("/api/login", async (
    [FromBody] LoginUserRequest request,
    IOptions<AuthConfiguration> authOptions,
    UserManager<User> userManager,
    SignInManager<User> signInManager,
    RoleManager<Role> roleManager) =>
{
    var user = await userManager.FindByEmailAsync(request.Email);
    if (user is null)
    {
        return Results.NotFound("User not found");
    }

    var result = await signInManager.CheckPasswordSignInAsync(user, request.Password, false);
    if (!result.Succeeded)
    {
        return Results.Unauthorized();
    }

    var roles = await userManager.GetRolesAsync(user);
    var userRole = roles.FirstOrDefault() ?? "user";

    var role = await roleManager.FindByNameAsync(userRole);
    var roleClaims = role is not null ? await roleManager.GetClaimsAsync(role) : [];

    var token = GenerateJwtToken(user, authOptions.Value, userRole, roleClaims);
    return Results.Ok(new { Token = token });
});
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

روند احراز هویت مراحل زیر را شامل می شود:

  1. شما باید بررسی کنید که آیا کاربر با تماس با شما وجود دارد FindByEmailAsync روش
  2. با تماس می توانید رمز عبور کاربر را بررسی کنید CheckPasswordSignInAsync روش از SignInManagerبشر
  3. با تماس می توانید نقش های کاربر را بدست آورید GetRolesAsync روش از UserManagerبشر
  4. با فراخوانی می توانید ادعاهای این نقش را دریافت کنید GetClaimsAsync روش از RoleManagerبشر
  5. در صورت بروز خطا در هنگام ورود – می توانید a را برگردانید BadRequest پاسخ با پیام خطا از هویت.

می توانید پس از ورود به سیستم موفقیت آمیز ، یک نشانه JWT صادر کنید:

private static string GenerateJwtToken(User user,
    AuthConfiguration authConfiguration,
    string userRole,
    IList<Claim> roleClaims)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authConfiguration.Key));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    List<Claim> claims = [
        new(JwtRegisteredClaimNames.Sub, user.Email!),
        new("userid", user.Id),
        new("role", userRole)
    ];

    foreach (var roleClaim in roleClaims)
    {
        claims.Add(new Claim(roleClaim.Type, roleClaim.Value));
    }

    var token = new JwtSecurityToken(
        issuer: authConfiguration.Issuer,
        audience: authConfiguration.Audience,
        claims: claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

هر نقش در برنامه من مجموعه ای از ادعاها را دارد ، من این ادعاها را به نشانه JWT اضافه می کنم.
در اینجا چیزی است که یک نشانه JWT صادر شده ممکن است به نظر برسد:

{
  "sub": "admin@test.com",
  "userid": "dc233fac-bace-4719-9a4f-853e199300d5",
  "role": "Admin",
  "users:create": "true",
  "users:update": "true",
  "users:delete": "true",
  "books:create": "true",
  "books:update": "true",
  "books:delete": "true",
  "exp": 1739481834,
  "iss": "DevTips",
  "aud": "DevTips"
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای مثال می توانید از ادعاها برای محدود کردن دسترسی به نقاط پایانی استفاده کنید:

app.MapPost("/api/books", Handle)
        .RequireAuthorization("books:create");

    app.MapDelete("/api/books/{id}", Handle)
        .RequireAuthorization("books:delete");

    app.MapPost("/api/users", Handle)
        .RequireAuthorization("users:create");

    app.MapDelete("/api/users/{id}", Handle)
        .RequireAuthorization("users:delete");
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

من نحوه تنظیم احراز هویت و مجوز را در هسته ASP.NET در اینجا توضیح دادم.

نحوه بذر داده های هویت: نقش ها و ادعاها را آغاز کنید

هنگامی که می خواهید یک برنامه را با نقش ها و ادعاهای پیش فرض تنظیم کنید ، بذر مفید است.
یک رویکرد مشترک به داده های بذر یک بار در راه اندازی برنامه است:

var app = builder.Build();

// Register middlewares...

// Create and seed database
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<BooksDbContext>();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();

    await DatabaseSeedService.SeedAsync(dbContext, userManager, roleManager);
}

await app.RunAsync();
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

public static class DatabaseSeedService
{
    public static async Task SeedAsync(BooksDbContext dbContext, UserManager<User> userManager,
        RoleManager<Role> roleManager)
    {
        await dbContext.Database.MigrateAsync();

        if (await dbContext.Users.AnyAsync())
        {
            return;
        }

        // Seed roles and claims here

        await dbContext.SaveChangesAsync();
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

شما می توانید از RoleManager برای مدیریت نقش ها و ادعاهای آنها:

var adminRole = new Role { Name = "Admin" };
var authorRole = new Role { Name = "Author" };

var result = await roleManager.CreateAsync(adminRole);
result = await roleManager.CreateAsync(authorRole);

result = await roleManager.AddClaimAsync(adminRole, new Claim("users:create", "true"));
result = await roleManager.AddClaimAsync(adminRole, new Claim("users:update", "true"));
result = await roleManager.AddClaimAsync(adminRole, new Claim("users:delete", "true"));

result = await roleManager.AddClaimAsync(adminRole, new Claim("books:create", "true"));
result = await roleManager.AddClaimAsync(adminRole, new Claim("books:update", "true"));
result = await roleManager.AddClaimAsync(adminRole, new Claim("books:delete", "true"));

result = await roleManager.AddClaimAsync(authorRole, new Claim("books:create", "true"));
result = await roleManager.AddClaimAsync(authorRole, new Claim("books:update", "true"));
result = await roleManager.AddClaimAsync(authorRole, new Claim("books:delete", "true"));
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا نحوه ایجاد یک کاربر پیش فرض در برنامه خود آورده شده است:

var adminUser = new User
{
    Id = Guid.NewGuid().ToString(),
    Email = "admin@test.com",
    UserName = "admin@test.com"
};

result = await userManager.CreateAsync(adminUser, "Test1234!");
result = await userManager.AddToRoleAsync(adminUser, "Admin");
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تغییر رمز عبور پیش فرض پس از ورود به سیستم برای اولین بار مهم است.

در وب سایت من: antondevtips.com من بهترین روش های .NET و معماری را به اشتراک می گذارم.
برای بهبود مهارت های دات نت خود در خبرنامه من مشترک شوید.
کد منبع را برای این خبرنامه به صورت رایگان بارگیری کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا