ASP.NET Core: Tạo ứng dụng web ASP.NET Core với dữ liệu người dùng được bảo vệ bằng ủy quyền (authorization)
Bài viết này sẽ hướng dẫn cách tạo một ứng dụng web ASP.NET Core với dữ liệu người dùng được bảo vệ bằng ủy quyền (authorization). Nó hiển thị danh sách liên hệ (contact) mà người dùng đã chứng thực (đã đăng ký) đã tạo. Có ba nhóm bảo mật:
- Người dùng đã đăng ký có thể xem tất cả dữ liệu đã được phê duyệt và có thể chỉnh sửa/xóa dữ liệu của chính họ.
- Người quản lý có thể phê duyệt hoặc từ chối dữ liệu liên hệ. Chỉ những người liên hệ được phê duyệt mới hiển thị với người dùng.
- Quản trị viên có thể phê duyệt/từ chối và chỉnh sửa/xóa bất kỳ dữ liệu nào.
Hình ảnh trong tài liệu này không khớp hoàn toàn với các mẫu mới nhất.
Trong hình ảnh sau đây, người dùng Rick (rick@example.com
) đã đăng nhập. Rick chỉ có thể xem các liên hệ đã được phê duyệt và Edit/Delete/Create New cho các liên hệ của mình. Chỉ bản ghi cuối cùng do Rick tạo mới hiển thị các liên kết Edit và Delete. Những người dùng khác sẽ không nhìn thấy bản ghi cuối cùng cho đến khi người quản lý hoặc quản trị viên thay đổi trạng thái thành "Approved".
Trong hình ảnh sau đây, manager@contoso.com
được đăng nhập và có vai trò là người quản lý:
Hình ảnh sau đây hiển thị chế độ xem chi tiết của người quản lý về một liên hệ:
Các nút Approve và Reject chỉ được hiển thị cho người quản lý và quản trị viên.
Trong hình ảnh sau đây, admin@contoso.com
được đăng nhập và ở vai trò quản trị viên:
Quản trị viên có tất cả các đặc quyền. Quản trị viên có thể đọc, chỉnh sửa hoặc xóa bất kỳ liên hệ nào và thay đổi trạng thái của liên hệ.
Ứng dụng này được tạo bằng cách xây dựng model Contact
sau:
public class Contact
{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
Mẫu chứa các trình xử lý ủy quyền sau:
ContactIsOwnerAuthorizationHandler
: Đảm bảo rằng người dùng chỉ có thể chỉnh sửa dữ liệu của họ.ContactManagerAuthorizationHandler
: Cho phép người quản lý phê duyệt hoặc từ chối liên hệ.ContactAdministratorsAuthorizationHandler
: Cho phép quản trị viên phê duyệt hoặc từ chối liên hệ và chỉnh sửa/xóa liên hệ.
Điều kiện tiên quyết
Hướng dẫn này là nâng cao. Bạn nên làm quen với:
Ứng dụng khởi đầu và hoàn thành
Tải xuống ứng dụng đã hoàn thành. Kiểm tra ứng dụng đã hoàn thiện để bạn làm quen với các tính năng bảo mật của ứng dụng đó.
Ứng dụng khởi đầu
Chạy ứng dụng, nhấn vào liên kết ContactManager và xác minh rằng bạn có thể tạo, chỉnh sửa và xóa một liên hệ. Để tạo ứng dụng khởi đầu, hãy xem Tạo ứng dụng khởi đầu.
Bảo mật dữ liệu người dùng
Các phần sau đây có tất cả các bước chính để tạo ứng dụng bảo mật dữ liệu người dùng. Bạn có thể thấy hữu ích khi tham khảo dự án đã hoàn thành.
Liên kết dữ liệu liên hệ với người dùng
Sử dụng ID người dùng ASP.NET Identity để đảm bảo người dùng có thể chỉnh sửa dữ liệu của họ chứ không phải dữ liệu của người dùng khác. Thêm OwnerID
và ContactStatus
vào model Contact
:
public class Contact
{
public int ContactId { get; set; }
// user ID from AspNetUser table.
public string? OwnerID { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string? Email { get; set; }
public ContactStatus Status { get; set; }
}
public enum ContactStatus
{
Submitted,
Approved,
Rejected
}
OwnerID
là ID của người dùng từ bảng AspNetUser
trong cơ sở dữ liệu Identity. Trường Status
xác định xem người dùng thông thường có thể xem được một liên hệ hay không.
Tạo một migration mới và cập nhật cơ sở dữ liệu:
dotnet ef migrations add userID_Status
dotnet ef database update
Thêm dịch vụ Vai trò (Role) vào Danh tính
Nạp AddRoles để thêm dịch vụ Vai trò:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Yêu cầu người dùng được chứng thực
Đặt chính sách ủy quyền dự phòng để yêu cầu người dùng được chứng thực:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Đoạn code được đánh dấu ở trên đó đặt chính sách ủy quyền dự phòng. Chính sách ủy quyền dự phòng yêu cầu tất cả người dùng phải được chứng thực, ngoại trừ Razor Pages, controller hoặc phương thức action có attribute ủy quyền. Ví dụ: Razor Pages, controller hoặc phương thức action có [AllowAnonymous]
hoặc [Authorize(PolicyName="MyPolicy")]
sử dụng attribute ủy quyền được áp dụng thay vì chính sách ủy quyền dự phòng.
RequireAuthenticatedUser thêm DenyAnonymousAuthorizationRequirement vào thể hiện hiện tại để buộc người dùng hiện tại phải được chứng thực.
Chính sách ủy quyền dự phòng:
- Được áp dụng cho tất cả các yêu cầu không chỉ định rõ ràng chính sách ủy quyền. Đối với các yêu cầu được phân phát bằng định tuyến điểm cuối, điều này bao gồm mọi điểm cuối không chỉ định attribute ủy quyền. Đối với các yêu cầu được phân phối bởi middleware khác sau middleware ủy quyền, chẳng hạn như các file tĩnh, thì chính sách này sẽ áp dụng cho tất cả các yêu cầu.
Việc đặt chính sách ủy quyền dự phòng để yêu cầu người dùng được chứng thực sẽ bảo vệ các Razor Pages và controller mới được thêm vào. Việc có ủy quyền được yêu cầu theo mặc định sẽ an toàn hơn việc dựa vào controller và Razor Pages mới để bao gồm attribute [Authorize]
.
Lớp AuthorizationOptions cũng chứa AuthorizationOptions.DefaultPolicy. DefaultPolicy
là chính sách được sử dụng với attribute [Authorize]
khi không có chính sách nào được chỉ định. [Authorize]
không chứa chính sách được đặt tên, không giống như [Authorize(PolicyName="MyPolicy")]
.
Để biết thêm thông tin về chính sách, hãy xem Ủy quyền dựa trên chính sách trong ASP.NET Core.
Một cách khác để controller MVC và Razor Pages yêu cầu tất cả người dùng phải được chứng thực là thêm bộ lọc ủy quyền được thể hiện như sau:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllers(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
var app = builder.Build();
Đoạn code trên sử dụng bộ lọc ủy quyền, cài đặt chính sách dự phòng sử dụng định tuyến điểm cuối. Đặt chính sách dự phòng là cách ưu tiên để yêu cầu tất cả người dùng phải được chứng thực.
Thêm AllowAnonymous vào các trang Index và Privacy để người dùng ẩn danh có thể lấy thông tin về trang web trước khi họ đăng ký:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages;
[AllowAnonymous]
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
Định cấu hình tài khoản thử nghiệm
Lớp SeedData
tạo hai tài khoản: quản trị viên và người quản lý. Sử dụng công cụ Secret Manager để đặt mật khẩu cho các tài khoản này. Đặt mật khẩu từ thư mục dự án (thư mục chứa Program.cs
):
dotnet user-secrets set SeedUserPW <PW>
Nếu mật khẩu mạnh không được chỉ định, một ngoại lệ sẽ được đưa ra khi SeedData.Initialize
được gọi.
Cập nhật ứng dụng để sử dụng mật khẩu kiểm tra:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");
await SeedData.Initialize(services, testUserPw);
}
Tạo tài khoản thử nghiệm và cập nhật danh bạ
Cập nhật phương thức Initialize
trong lớp SeedData
để tạo tài khoản thử nghiệm:
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
// For sample purposes seed both with the same password.
// Password is set with the following:
// dotnet user-secrets set SeedUserPW <pw>
// The admin user can do anything
var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);
// allowed user can create and edit contacts that they create
var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);
SeedDB(context, adminID);
}
}
private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(UserName);
if (user == null)
{
user = new IdentityUser
{
UserName = UserName,
EmailConfirmed = true
};
await userManager.CreateAsync(user, testUserPw);
}
if (user == null)
{
throw new Exception("The password is probably not strong enough!");
}
return user.Id;
}
private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
string uid, string role)
{
var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
if (roleManager == null)
{
throw new Exception("roleManager null");
}
IdentityResult IR;
if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
//if (userManager == null)
//{
// throw new Exception("userManager is null");
//}
var user = await userManager.FindByIdAsync(uid);
if (user == null)
{
throw new Exception("The testUserPw password was probably not strong enough!");
}
IR = await userManager.AddToRoleAsync(user, role);
return IR;
}
Thêm ID người dùng quản trị viên và ContactStatus
vào danh bạ. Đặt một trong các địa chỉ liên hệ là "Đã gửi" và một địa chỉ liên hệ là "Đã từ chối". Thêm ID người dùng và trạng thái vào tất cả các liên hệ. Chỉ có một liên hệ được hiển thị:
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com",
Status = ContactStatus.Approved,
OwnerID = adminID
},
Tạo trình xử lý ủy quyền của chủ sở hữu, người quản lý và quản trị viên
Tạo một lớp ContactIsOwnerAuthorizationHandler
trong thư mục Authorization. ContactIsOwnerAuthorizationHandler
xác minh rằng người dùng hành động trên một tài nguyên sở hữu tài nguyên đó.
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace ContactManager.Authorization
{
public class ContactIsOwnerAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
UserManager<IdentityUser> _userManager;
public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser>
userManager)
{
_userManager = userManager;
}
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for CRUD permission, return.
if (requirement.Name != Constants.CreateOperationName &&
requirement.Name != Constants.ReadOperationName &&
requirement.Name != Constants.UpdateOperationName &&
requirement.Name != Constants.DeleteOperationName )
{
return Task.CompletedTask;
}
if (resource.OwnerID == _userManager.GetUserId(context.User))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
ContactIsOwnerAuthorizationHandler
gọi context.Succeed nếu người dùng được chứng thực hiện tại là chủ sở hữu liên hệ. Trình xử lý ủy quyền nói chung:
- Gọi
context.Succeed
khi đạt yêu cầu. - Trả lại
Task.CompletedTask
khi không đạt yêu cầu. Việc trả vềTask.CompletedTask
mà không gọi trướccontext.Success
haycontext.Fail
, thì không phải là thành công hay thất bại, nó cho phép các trình xử lý ủy quyền khác chạy.
Nếu bạn cần thất bại một cách rõ ràng, hãy gọi context.Fail.
Ứng dụng cho phép chủ sở hữu liên hệ edit/delete/create dữ liệu của riêng họ. ContactIsOwnerAuthorizationHandler
không cần kiểm tra thao tác được truyền trong tham số yêu cầu.
Tạo trình xử lý ủy quyền của người quản lý
Tạo một lớp ContactManagerAuthorizationHandler
trong thư mục Authorization. ContactManagerAuthorizationHandler
xác minh người dùng hành động trên tài nguyên là người quản lý. Chỉ người quản lý mới có thể phê duyệt hoặc từ chối các thay đổi nội dung (mới hoặc đã thay đổi).
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
namespace ContactManager.Authorization
{
public class ContactManagerAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for approval/reject, return.
if (requirement.Name != Constants.ApproveOperationName &&
requirement.Name != Constants.RejectOperationName)
{
return Task.CompletedTask;
}
// Managers can approve or reject.
if (context.User.IsInRole(Constants.ContactManagersRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
Tạo trình xử lý ủy quyền quản trị viên
Tạo một lớp ContactAdministratorsAuthorizationHandler
trong thư mục Authorization. ContactAdministratorsAuthorizationHandler
xác minh người dùng hành động trên tài nguyên là quản trị viên. Người quản trị có thể thực hiện mọi thao tác.
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public class ContactAdministratorsAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null)
{
return Task.CompletedTask;
}
// Administrators can do anything.
if (context.User.IsInRole(Constants.ContactAdministratorsRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
Đăng ký trình xử lý ủy quyền
Các dịch vụ sử dụng Entity Framework Core phải được đăng ký để chèn phụ thuộc (Dependency Injection) bằng AddScoped. ContactIsOwnerAuthorizationHandler
sử dụng ASP.NET Core Identity, được xây dựng trên Entity Framework Core. Đăng ký các trình xử lý với collection dịch vụ để chúng sẵn sàng cho việc ContactsController
thông qua chèn phụ thuộc. Thêm đoạn code sau vào cuối của ConfigureServices
:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");
await SeedData.Initialize(services, testUserPw);
}
ContactAdministratorsAuthorizationHandler
và ContactManagerAuthorizationHandler
được thêm vào dưới dạng các singleton. Chúng là những singleton vì chúng không sử dụng EF và tất cả thông tin cần thiết đều có trong tham số Context
của phương thức HandleRequirementAsync
.
Hỗ trợ ủy quyền
Trong phần này, bạn cập nhật Razor Pages và thêm lớp yêu cầu hoạt động.
Xem lại lớp yêu cầu về hoạt động liên hệ
Xem lại lớp ContactOperations
. Lớp này chứa các yêu cầu mà ứng dụng hỗ trợ:
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public static class ContactOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
public static OperationAuthorizationRequirement Approve =
new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
public static OperationAuthorizationRequirement Reject =
new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
}
public class Constants
{
public static readonly string CreateOperationName = "Create";
public static readonly string ReadOperationName = "Read";
public static readonly string UpdateOperationName = "Update";
public static readonly string DeleteOperationName = "Delete";
public static readonly string ApproveOperationName = "Approve";
public static readonly string RejectOperationName = "Reject";
public static readonly string ContactAdministratorsRole =
"ContactAdministrators";
public static readonly string ContactManagersRole = "ContactManagers";
}
}
Tạo một lớp cơ sở cho Razor Pages Contacts
Tạo một lớp cơ sở chứa các dịch vụ được sử dụng trong Razor Pages Contacts. Lớp cơ sở đặt code khởi tạo ở một vị trí:
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages.Contacts
{
public class DI_BasePageModel : PageModel
{
protected ApplicationDbContext Context { get; }
protected IAuthorizationService AuthorizationService { get; }
protected UserManager<IdentityUser> UserManager { get; }
public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
Trong đoạn code trên:
- Thêm dịch vụ
IAuthorizationService
để truy cập vào trình xử lý ủy quyền. - Thêm dịch vụ Nhận dạng
UserManager
. - Thêm file
ApplicationDbContext
.
Cập nhật CreateModel
Cập nhật model trang tạo:
- Hàm tạo để sử dụng lớp cơ sở
DI_BasePageModel
. - Phương thức
OnPostAsync
để:- Thêm ID người dùng vào model
Contact
. - Gọi trình xử lý ủy quyền để xác minh người dùng có quyền tạo liên hệ.
- Thêm ID người dùng vào model
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace ContactManager.Pages.Contacts
{
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Contact.OwnerID = UserManager.GetUserId(User);
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Create);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Add(Contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Cập nhật IndexModel
Cập nhật phương thức OnGetAsync
để chỉ những người liên hệ được phê duyệt mới được hiển thị cho người dùng thông thường:
public class IndexModel : DI_BasePageModel
{
public IndexModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public IList<Contact> Contact { get; set; }
public async Task OnGetAsync()
{
var contacts = from c in Context.Contact
select c;
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
// Only approved contacts are shown UNLESS you're authorized to see them
// or you are the owner.
if (!isAuthorized)
{
contacts = contacts.Where(c => c.Status == ContactStatus.Approved
|| c.OwnerID == currentUserId);
}
Contact = await contacts.ToListAsync();
}
}
Cập nhật EditModel
Thêm trình xử lý ủy quyền để xác minh người dùng sở hữu liên hệ. Vì ủy quyền tài nguyên đang được xác thực nên attribute [Authorize]
là không đủ. Ứng dụng không có quyền truy cập vào tài nguyên khi đánh giá các attribute. Ủy quyền dựa trên tài nguyên phải là bắt buộc. Việc kiểm tra phải được thực hiện sau khi ứng dụng có quyền truy cập vào tài nguyên, bằng cách tải tài nguyên đó trong model trang hoặc bằng cách tải tài nguyên đó trong chính trình xử lý. Bạn thường xuyên truy cập tài nguyên bằng cách chuyển khóa tài nguyên.
public class EditModel : DI_BasePageModel
{
public EditModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
Contact = contact;
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch Contact from DB to get OwnerID.
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Contact.OwnerID = contact.OwnerID;
Context.Attach(Contact).State = EntityState.Modified;
if (Contact.Status == ContactStatus.Approved)
{
// If the contact is updated after approval,
// and the user cannot approve,
// set the status back to submitted so the update can be
// checked and approved.
var canApprove = await AuthorizationService.AuthorizeAsync(User,
Contact,
ContactOperations.Approve);
if (!canApprove.Succeeded)
{
Contact.Status = ContactStatus.Submitted;
}
}
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Cập nhật DeleteModel
Cập nhật model trang delete để sử dụng trình xử lý ủy quyền nhằm xác minh người dùng có quyền xóa đối với người liên hệ.
public class DeleteModel : DI_BasePageModel
{
public DeleteModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Remove(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Chèn (Inject) dịch vụ ủy quyền vào View
Hiện tại, giao diện người dùng hiển thị các liên kết chỉnh sửa và xóa đối với các liên hệ mà người dùng không thể sửa đổi.
Đưa dịch vụ ủy quyền vào file Pages/_ViewImports.cshtml
để tất cả các view đều có thể sử dụng dịch vụ này:
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
Cập nhật các liên kết Edit và Delete trong Pages/Contacts/Index.cshtml
để chúng chỉ được hiển thị cho những người dùng có quyền thích hợp:
@page
@model ContactManager.Pages.Contacts.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Address)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].City)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].State)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Zip)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Status)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Contact) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Status)
</td>
<td>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Delete)).Succeeded)
{
<text> | </text>
<a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
}
</td>
</tr>
}
</tbody>
</table>
Cảnh báo
Việc ẩn liên kết khỏi những người dùng không có quyền thay đổi dữ liệu sẽ không bảo mật ứng dụng. Việc ẩn các liên kết giúp ứng dụng thân thiện hơn với người dùng bằng cách chỉ hiển thị các liên kết hợp lệ. Người dùng có thể hack các URL đã tạo để thực hiện các thao tác chỉnh sửa và xóa trên dữ liệu mà họ không sở hữu. Razor Pages hoặc controller phải thực thi kiểm tra quyền truy cập để bảo mật dữ liệu.
Cập nhật chi tiết
Cập nhật view chi tiết để người quản lý có thể phê duyệt hoặc từ chối liên hệ:
@*Preceding markup omitted for brevity.*@
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Contact.Email)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Contact.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Contact.Status)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Status)
</dd>
</dl>
</div>
@if (Model.Contact.Status != ContactStatus.Approved)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Approve)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Approved" />
<button type="submit" class="btn btn-xs btn-success">Approve</button>
</form>
}
}
@if (Model.Contact.Status != ContactStatus.Rejected)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Reject)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Rejected" />
<button type="submit" class="btn btn-xs btn-danger">Reject</button>
</form>
}
}
<div>
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Index">Back to List</a>
</div>
Cập nhật page model chi tiết
public class DetailsModel : DI_BasePageModel
{
public DetailsModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
{
var contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var contactOperation = (status == ContactStatus.Approved)
? ContactOperations.Approve
: ContactOperations.Reject;
var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
contactOperation);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
contact.Status = status;
Context.Contact.Update(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Thêm hoặc xóa người dùng vào một vai trò (role)
Xem vấn đề này để biết thông tin về:
- Xóa đặc quyền khỏi người dùng. Ví dụ: tắt tiếng người dùng trong ứng dụng trò chuyện.
- Thêm đặc quyền cho người dùng.
Sự khác biệt giữa Thử thách (Challenge) và Cấm (Forbid)
Ứng dụng này đặt chính sách mặc định để yêu cầu người dùng được chứng thực. Đoạn mã sau cho phép người dùng ẩn danh. Người dùng ẩn danh được phép thể hiện sự khác biệt giữa Thử thách và Cấm.
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
public Details2Model(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
if (!User.Identity!.IsAuthenticated)
{
return Challenge();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
}
Trong đoạn code trên:
- Khi người dùng không được chứng thực, thì một
ChallengeResult
sẽ được trả về. KhiChallengeResult
được trả về thì người dùng sẽ được chuyển hướng đến trang đăng nhập. - Khi người dùng được chứng thực nhưng không được ủy quyền, thì một
ForbidResult
sẽ được trả về. KhiForbidResult
được trả về, thì người dùng sẽ được chuyển hướng đến trang bị từ chối truy cập.
Kiểm tra ứng dụng đã hoàn thành
Nếu bạn chưa đặt mật khẩu cho tài khoản người dùng hạt giống, hãy sử dụng công cụ Trình quản lý bí mật để đặt mật khẩu:
-
Chọn mật khẩu mạnh: Sử dụng tám ký tự trở lên và ít nhất một ký tự viết hoa, số và ký hiệu. Ví dụ:
Passw0rd!
đáp ứng các yêu cầu về mật khẩu mạnh. -
Thực hiện lệnh sau từ thư mục của dự án, trong đó
<PW>
là mật khẩu:dotnet user-secrets set SeedUserPW <PW>
Nếu ứng dụng có danh bạ:
- Xóa tất cả các bản ghi trong bảng
Contact
. - Khởi động lại ứng dụng để tạo cơ sở dữ liệu.
Một cách dễ dàng để kiểm tra ứng dụng đã hoàn thiện là khởi chạy ba trình duyệt khác nhau (hoặc phiên ẩn danh/InPrivate). Trong một trình duyệt, hãy đăng ký một người dùng mới (ví dụ: test@example.com
). Đăng nhập vào mỗi trình duyệt bằng một người dùng khác nhau. Xác minh các hoạt động sau:
- Người dùng đã đăng ký có thể xem tất cả dữ liệu liên hệ đã được phê duyệt.
- Người dùng đã đăng ký có thể chỉnh sửa/xóa dữ liệu của riêng họ.
- Người quản lý có thể phê duyệt/từ chối dữ liệu liên hệ. View
Details
hiển thị các nút Approve và Reject. - Quản trị viên có thể phê duyệt/từ chối và chỉnh sửa/xóa tất cả dữ liệu.
Người dùng | Phê duyệt hoặc từ chối liên hệ | Tùy chọn |
---|---|---|
test@example.com | No | Chỉnh sửa và xóa dữ liệu của họ. |
manager@contoso.com | Yes | Chỉnh sửa và xóa dữ liệu của họ. |
quản trị@contoso.com | Yes | Chỉnh sửa và xóa tất cả dữ liệu. |
Tạo một liên hệ trong trình duyệt của quản trị viên. Sao chép URL để xóa và chỉnh sửa từ liên hệ của quản trị viên. Dán các liên kết này vào trình duyệt của người dùng thử nghiệm để xác minh rằng người dùng thử nghiệm không thể thực hiện các thao tác này.
Tạo ứng dụng khởi đầu (starter app)
- Tạo ứng dụng Razor Pages có tên "ContactManager"
- Tạo ứng dụng bằng Individual User Accounts.
- Đặt tên là "ContactManager" để namespace khớp với namespace được sử dụng trong mẫu.
-uld
chỉ định LocalDB thay vì SQLite
dotnet new webapp -o ContactManager -au Individual -uld
- Thêm
Models/Contact.cs
: secure-data\samples\starter6\ContactManager\Models\Contact.cs
using System.ComponentModel.DataAnnotations;
namespace ContactManager.Models
{
public class Contact
{
public int ContactId { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string? Email { get; set; }
}
}
- Scaffold model
Contact
. - Tạo migration ban đầu và cập nhật cơ sở dữ liệu:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update
Ghi chú
Theo mặc định, kiến trúc của các tệp nhị phân .NET cần cài đặt đại diện cho kiến trúc hệ điều hành hiện đang chạy. Để chỉ định kiến trúc hệ điều hành khác, hãy xem cài đặt công cụ dotnet, tùy chọn --arch. Để biết thêm thông tin, hãy xem vấn đề GitHub dotnet/AspNetCore.Docs #29262.
- Cập nhật link ContactManager trong file
Pages/Shared/_Layout.cshtml
:
<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
- Kiểm tra ứng dụng bằng cách tạo, chỉnh sửa và xóa liên hệ
Tạo cơ sở dữ liệu
Thêm lớp SeedData vào thư mục Data:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, testUserPw);
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
Gọi SeedData.Initialize
từ Program.cs
:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await SeedData.Initialize(services);
}
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Kiểm tra xem ứng dụng đã tạo cơ sở dữ liệu chưa. Nếu có bất kỳ hàng nào trong cơ sở dữ liệu liên hệ thì phương thức hạt giống sẽ không chạy.
Tài nguyên bổ sung
- Hướng dẫn: Xây dựng ứng dụng Cơ sở dữ liệu ASP.NET Core và Azure SQL trong App Service Azure
- Phòng thí nghiệm ủy quyền ASP.NET Core. Phòng thí nghiệm này đi sâu vào chi tiết hơn về các tính năng bảo mật được giới thiệu trong hướng dẫn này.
- Giới thiệu về ủy quyền trong ASP.NET Core
- Ủy quyền dựa trên chính sách tùy chỉnh