第十九章:安全性與資料保護 - 習題解答
本文件提供第十九章實戰練習的完整解答,涵蓋權限系統、資料加密和 GDPR 合規。
練習 1:實作權限系統
題目
- 定義完整的權限樹(至少 10 個權限)。
- 為不同角色分配權限。
- 測試未授權存取會被拒絕。
解答
步驟 1:定義權限結構
csharp
// Application.Contracts/Permissions/BookStorePermissions.cs
namespace BookStore.Permissions
{
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
// 書籍管理
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string Export = Default + ".Export";
}
// 作者管理
public static class Authors
{
public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
// 訂單管理
public static class Orders
{
public const string Default = GroupName + ".Orders";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string Approve = Default + ".Approve";
public const string ViewAll = Default + ".ViewAll"; // 查看所有訂單(管理員)
}
// 報表
public static class Reports
{
public const string Default = GroupName + ".Reports";
public const string Sales = Default + ".Sales";
public const string Inventory = Default + ".Inventory";
public const string Advanced = Default + ".Advanced";
}
// 系統設定
public static class Settings
{
public const string Default = GroupName + ".Settings";
public const string Manage = Default + ".Manage";
}
}
}步驟 2:定義權限提供者
csharp
// Application.Contracts/Permissions/BookStorePermissionDefinitionProvider.cs
using BookStore.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace BookStore.Permissions
{
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var bookStoreGroup = context.AddGroup(
BookStorePermissions.GroupName,
L("Permission:BookStore"));
// 書籍權限
var booksPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Books.Default,
L("Permission:Books"));
booksPermission.AddChild(
BookStorePermissions.Books.Create,
L("Permission:Books.Create"));
booksPermission.AddChild(
BookStorePermissions.Books.Edit,
L("Permission:Books.Edit"));
booksPermission.AddChild(
BookStorePermissions.Books.Delete,
L("Permission:Books.Delete"));
booksPermission.AddChild(
BookStorePermissions.Books.Export,
L("Permission:Books.Export"));
// 作者權限
var authorsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Authors.Default,
L("Permission:Authors"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Create,
L("Permission:Authors.Create"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Edit,
L("Permission:Authors.Edit"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Delete,
L("Permission:Authors.Delete"));
// 訂單權限
var ordersPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Orders.Default,
L("Permission:Orders"));
ordersPermission.AddChild(
BookStorePermissions.Orders.Create,
L("Permission:Orders.Create"));
ordersPermission.AddChild(
BookStorePermissions.Orders.Edit,
L("Permission:Orders.Edit"));
ordersPermission.AddChild(
BookStorePermissions.Orders.Delete,
L("Permission:Orders.Delete"));
ordersPermission.AddChild(
BookStorePermissions.Orders.Approve,
L("Permission:Orders.Approve"));
ordersPermission.AddChild(
BookStorePermissions.Orders.ViewAll,
L("Permission:Orders.ViewAll"));
// 報表權限
var reportsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Reports.Default,
L("Permission:Reports"));
reportsPermission.AddChild(
BookStorePermissions.Reports.Sales,
L("Permission:Reports.Sales"));
reportsPermission.AddChild(
BookStorePermissions.Reports.Inventory,
L("Permission:Reports.Inventory"));
reportsPermission.AddChild(
BookStorePermissions.Reports.Advanced,
L("Permission:Reports.Advanced"));
// 系統設定權限
var settingsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Settings.Default,
L("Permission:Settings"));
settingsPermission.AddChild(
BookStorePermissions.Settings.Manage,
L("Permission:Settings.Manage"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
}步驟 3:在本地化資源中定義權限名稱
json
// Domain.Shared/Localization/BookStore/zh-Hant.json
{
"culture": "zh-Hant",
"texts": {
"Permission:BookStore": "書店管理",
"Permission:Books": "書籍管理",
"Permission:Books.Create": "建立書籍",
"Permission:Books.Edit": "編輯書籍",
"Permission:Books.Delete": "刪除書籍",
"Permission:Books.Export": "匯出書籍",
"Permission:Authors": "作者管理",
"Permission:Authors.Create": "建立作者",
"Permission:Authors.Edit": "編輯作者",
"Permission:Authors.Delete": "刪除作者",
"Permission:Orders": "訂單管理",
"Permission:Orders.Create": "建立訂單",
"Permission:Orders.Edit": "編輯訂單",
"Permission:Orders.Delete": "刪除訂單",
"Permission:Orders.Approve": "核准訂單",
"Permission:Orders.ViewAll": "查看所有訂單",
"Permission:Reports": "報表",
"Permission:Reports.Sales": "銷售報表",
"Permission:Reports.Inventory": "庫存報表",
"Permission:Reports.Advanced": "進階報表",
"Permission:Settings": "系統設定",
"Permission:Settings.Manage": "管理系統設定"
}
}步驟 4:為角色分配權限
csharp
// Domain/Data/BookStoreDataSeedContributor.cs
using System;
using System.Threading.Tasks;
using BookStore.Permissions;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.PermissionManagement;
namespace BookStore.Data
{
public class BookStoreDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IIdentityRoleRepository _roleRepository;
private readonly IPermissionManager _permissionManager;
private readonly RoleManager<IdentityRole> _roleManager;
public BookStoreDataSeedContributor(
IIdentityRoleRepository roleRepository,
IPermissionManager permissionManager,
RoleManager<IdentityRole> roleManager)
{
_roleRepository = roleRepository;
_permissionManager = permissionManager;
_roleManager = roleManager;
}
public async Task SeedAsync(DataSeedContext context)
{
await SeedRolesAndPermissionsAsync();
}
private async Task SeedRolesAndPermissionsAsync()
{
// 建立角色
await CreateRoleAsync("Admin", "管理員");
await CreateRoleAsync("Manager", "經理");
await CreateRoleAsync("Staff", "員工");
await CreateRoleAsync("Customer", "客戶");
// 為管理員分配所有權限
await GrantPermissionsToRoleAsync("Admin", new[]
{
BookStorePermissions.Books.Default,
BookStorePermissions.Books.Create,
BookStorePermissions.Books.Edit,
BookStorePermissions.Books.Delete,
BookStorePermissions.Books.Export,
BookStorePermissions.Authors.Default,
BookStorePermissions.Authors.Create,
BookStorePermissions.Authors.Edit,
BookStorePermissions.Authors.Delete,
BookStorePermissions.Orders.Default,
BookStorePermissions.Orders.Create,
BookStorePermissions.Orders.Edit,
BookStorePermissions.Orders.Delete,
BookStorePermissions.Orders.Approve,
BookStorePermissions.Orders.ViewAll,
BookStorePermissions.Reports.Default,
BookStorePermissions.Reports.Sales,
BookStorePermissions.Reports.Inventory,
BookStorePermissions.Reports.Advanced,
BookStorePermissions.Settings.Default,
BookStorePermissions.Settings.Manage
});
// 為經理分配部分權限
await GrantPermissionsToRoleAsync("Manager", new[]
{
BookStorePermissions.Books.Default,
BookStorePermissions.Books.Create,
BookStorePermissions.Books.Edit,
BookStorePermissions.Authors.Default,
BookStorePermissions.Authors.Create,
BookStorePermissions.Authors.Edit,
BookStorePermissions.Orders.Default,
BookStorePermissions.Orders.Create,
BookStorePermissions.Orders.Edit,
BookStorePermissions.Orders.Approve,
BookStorePermissions.Orders.ViewAll,
BookStorePermissions.Reports.Default,
BookStorePermissions.Reports.Sales,
BookStorePermissions.Reports.Inventory
});
// 為員工分配基本權限
await GrantPermissionsToRoleAsync("Staff", new[]
{
BookStorePermissions.Books.Default,
BookStorePermissions.Authors.Default,
BookStorePermissions.Orders.Default,
BookStorePermissions.Orders.Create,
BookStorePermissions.Reports.Default,
BookStorePermissions.Reports.Sales
});
// 為客戶分配最少權限
await GrantPermissionsToRoleAsync("Customer", new[]
{
BookStorePermissions.Books.Default,
BookStorePermissions.Orders.Default,
BookStorePermissions.Orders.Create
});
}
private async Task CreateRoleAsync(string name, string displayName)
{
var role = await _roleRepository.FindByNormalizedNameAsync(
_roleManager.NormalizeKey(name));
if (role == null)
{
role = new IdentityRole(Guid.NewGuid(), name)
{
IsDefault = false,
IsPublic = true
};
await _roleRepository.InsertAsync(role);
}
}
private async Task GrantPermissionsToRoleAsync(string roleName, string[] permissions)
{
var role = await _roleRepository.FindByNormalizedNameAsync(
_roleManager.NormalizeKey(roleName));
if (role == null)
{
return;
}
foreach (var permission in permissions)
{
await _permissionManager.SetForRoleAsync(
roleName,
permission,
true);
}
}
}
}步驟 5:在 Application Service 中使用權限
csharp
// Application/Books/BookAppService.cs
using Microsoft.AspNetCore.Authorization;
using BookStore.Permissions;
public class BookAppService : ApplicationService, IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository) : base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
[Authorize(BookStorePermissions.Books.Export)]
public async Task<byte[]> ExportToCsvAsync()
{
var books = await Repository.GetListAsync();
// 匯出邏輯...
return new byte[0];
}
}步驟 6:測試未授權存取
csharp
// Test/Application/Books/BookAppService_AuthorizationTests.cs
using System;
using System.Threading.Tasks;
using BookStore.Books;
using BookStore.Permissions;
using Shouldly;
using Volo.Abp.Authorization;
using Xunit;
namespace BookStore.Application.Books
{
public class BookAppService_AuthorizationTests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_AuthorizationTests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task CreateAsync_WithoutPermission_ShouldThrow()
{
// Arrange
// 登入為沒有建立權限的使用者
await LoginAsCustomerAsync();
var input = new CreateUpdateBookDto
{
Name = "Test Book",
Type = BookType.Fiction,
PublishDate = DateTime.Now,
Price = 10f
};
// Act & Assert
await Should.ThrowAsync<AbpAuthorizationException>(async () =>
{
await _bookAppService.CreateAsync(input);
});
}
[Fact]
public async Task CreateAsync_WithPermission_ShouldSucceed()
{
// Arrange
// 登入為有建立權限的使用者(管理員)
await LoginAsAdminAsync();
var input = new CreateUpdateBookDto
{
Name = "Test Book",
Type = BookType.Fiction,
PublishDate = DateTime.Now,
Price = 10f
};
// Act
var result = await _bookAppService.CreateAsync(input);
// Assert
result.ShouldNotBeNull();
result.Name.ShouldBe("Test Book");
}
[Fact]
public async Task DeleteAsync_WithoutPermission_ShouldThrow()
{
// Arrange
await LoginAsStaffAsync(); // 員工沒有刪除權限
// Act & Assert
await Should.ThrowAsync<AbpAuthorizationException>(async () =>
{
await _bookAppService.DeleteAsync(Guid.NewGuid());
});
}
private async Task LoginAsAdminAsync()
{
// 模擬管理員登入
// 實作取決於您的測試基礎設施
}
private async Task LoginAsCustomerAsync()
{
// 模擬客戶登入
}
private async Task LoginAsStaffAsync()
{
// 模擬員工登入
}
}
}練習 2:資料加密
題目
- 為
User實體的IdCardNumber欄位實作自動加密。 - 驗證資料庫中儲存的是密文。
解答
步驟 1:配置 Data Protection
csharp
// Web/BookStoreWebModule.cs
using Microsoft.AspNetCore.DataProtection;
public class BookStoreWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "keys")))
.SetApplicationName("BookStore")
.SetDefaultKeyLifetime(TimeSpan.FromDays(90));
// 生產環境應使用憑證保護金鑰
if (!context.Services.GetHostingEnvironment().IsDevelopment())
{
// context.Services.AddDataProtection()
// .ProtectKeysWithCertificate(certificate);
}
}
}步驟 2:建立加密 Value Converter
csharp
// EntityFrameworkCore/ValueConverters/EncryptedStringConverter.cs
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BookStore.EntityFrameworkCore.ValueConverters
{
public class EncryptedStringConverter : ValueConverter<string, string>
{
public EncryptedStringConverter(IDataProtector protector, ConverterMappingHints mappingHints = null)
: base(
v => protector.Protect(v ?? string.Empty),
v => protector.Unprotect(v ?? string.Empty),
mappingHints)
{
}
}
}步驟 3:在 DbContext 中配置加密
csharp
// EntityFrameworkCore/BookStoreDbContext.cs
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using BookStore.EntityFrameworkCore.ValueConverters;
using Volo.Abp.Identity;
namespace BookStore.EntityFrameworkCore
{
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
private readonly IDataProtectionProvider _dataProtectionProvider;
public BookStoreDbContext(
DbContextOptions<BookStoreDbContext> options,
IDataProtectionProvider dataProtectionProvider)
: base(options)
{
_dataProtectionProvider = dataProtectionProvider;
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureBookStore();
// 配置加密欄位
ConfigureEncryption(builder);
}
private void ConfigureEncryption(ModelBuilder builder)
{
var protector = _dataProtectionProvider.CreateProtector("BookStore.PersonalData");
builder.Entity<IdentityUser>(b =>
{
// 加密身分證字號
b.Property(e => e.GetType().GetProperty("IdCardNumber")?.GetValue(e) as string)
.HasConversion(new EncryptedStringConverter(protector))
.HasColumnName("IdCardNumber");
});
// 如果有自訂的 User 實體
builder.Entity<AppUser>(b =>
{
b.Property(e => e.IdCardNumber)
.HasConversion(new EncryptedStringConverter(protector))
.IsRequired(false);
b.Property(e => e.CreditCardNumber)
.HasConversion(new EncryptedStringConverter(protector))
.IsRequired(false);
});
}
}
}步驟 4:擴展 IdentityUser 或建立自訂 User 實體
csharp
// Domain/Users/AppUser.cs
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.Identity;
namespace BookStore.Users
{
public class AppUser : FullAuditedAggregateRoot<Guid>
{
public Guid IdentityUserId { get; set; }
/// <summary>
/// 身分證字號(將被加密)
/// </summary>
public string IdCardNumber { get; set; }
/// <summary>
/// 信用卡號(將被加密)
/// </summary>
public string CreditCardNumber { get; set; }
/// <summary>
/// 電話號碼(明文)
/// </summary>
public string PhoneNumber { get; set; }
protected AppUser()
{
}
public AppUser(Guid id, Guid identityUserId, string idCardNumber = null)
: base(id)
{
IdentityUserId = identityUserId;
IdCardNumber = idCardNumber;
}
public void UpdateIdCardNumber(string idCardNumber)
{
IdCardNumber = idCardNumber;
}
}
}步驟 5:建立 Migration
bash
cd src/BookStore.EntityFrameworkCore
dotnet ef migrations add AddedEncryptedFields
dotnet ef database update步驟 6:驗證加密
csharp
// Test/EntityFrameworkCore/EncryptionTests.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using BookStore.Users;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
namespace BookStore.EntityFrameworkCore
{
public class EncryptionTests : BookStoreEntityFrameworkCoreTestBase
{
private readonly IRepository<AppUser, Guid> _userRepository;
private readonly BookStoreDbContext _dbContext;
public EncryptionTests()
{
_userRepository = GetRequiredService<IRepository<AppUser, Guid>>();
_dbContext = GetRequiredService<BookStoreDbContext>();
}
[Fact]
public async Task IdCardNumber_ShouldBeEncryptedInDatabase()
{
// Arrange
var plainIdCard = "A123456789";
var user = new AppUser(
Guid.NewGuid(),
Guid.NewGuid(),
plainIdCard);
// Act
await _userRepository.InsertAsync(user);
await _dbContext.SaveChangesAsync();
// Assert - 從應用程式讀取應該是明文
var userFromApp = await _userRepository.GetAsync(user.Id);
userFromApp.IdCardNumber.ShouldBe(plainIdCard);
// Assert - 直接從資料庫讀取應該是密文
var connection = _dbContext.Database.GetDbConnection();
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = $"SELECT IdCardNumber FROM AppUsers WHERE Id = '{user.Id}'";
var encryptedValue = (string)await command.ExecuteScalarAsync();
// 密文應該與明文不同
encryptedValue.ShouldNotBe(plainIdCard);
// 密文應該是 Base64 編碼的字串
encryptedValue.ShouldNotBeNullOrWhiteSpace();
encryptedValue.Length.ShouldBeGreaterThan(plainIdCard.Length);
}
}
}
}練習 3:GDPR 合規
題目
- 實作完整的資料刪除流程。
- 實作資料匯出功能(Right to Data Portability)。
解答
步驟 1:實作資料刪除服務
csharp
// Domain/Users/UserDataDeletionService.cs
using System;
using System.Threading.Tasks;
using BookStore.Orders;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
using Volo.Abp.Uow;
namespace BookStore.Users
{
public class UserDataDeletionService : ITransientDependency
{
private readonly IRepository<AppUser, Guid> _appUserRepository;
private readonly IIdentityUserRepository _identityUserRepository;
private readonly IRepository<Order, Guid> _orderRepository;
private readonly ILogger<UserDataDeletionService> _logger;
public UserDataDeletionService(
IRepository<AppUser, Guid> appUserRepository,
IIdentityUserRepository identityUserRepository,
IRepository<Order, Guid> orderRepository,
ILogger<UserDataDeletionService> logger)
{
_appUserRepository = appUserRepository;
_identityUserRepository = identityUserRepository;
_orderRepository = orderRepository;
_logger = logger;
}
[UnitOfWork]
public async Task DeleteUserDataAsync(Guid userId)
{
_logger.LogInformation("開始刪除使用者 {UserId} 的個人資料", userId);
// 1. 匿名化 Identity User
var identityUser = await _identityUserRepository.GetAsync(userId);
await AnonymizeIdentityUserAsync(identityUser);
// 2. 匿名化 App User
var appUser = await _appUserRepository.FirstOrDefaultAsync(u => u.IdentityUserId == userId);
if (appUser != null)
{
await AnonymizeAppUserAsync(appUser);
}
// 3. 匿名化訂單資料
await AnonymizeUserOrdersAsync(userId);
_logger.LogInformation("使用者 {UserId} 的個人資料已成功刪除", userId);
}
private async Task AnonymizeIdentityUserAsync(IdentityUser user)
{
user.SetEmail($"deleted_{user.Id}@anonymized.local");
user.SetPhoneNumber(null);
user.Name = "已刪除的使用者";
user.Surname = "";
user.IsActive = false;
await _identityUserRepository.UpdateAsync(user);
}
private async Task AnonymizeAppUserAsync(AppUser user)
{
user.IdCardNumber = null;
user.CreditCardNumber = null;
user.PhoneNumber = null;
await _appUserRepository.UpdateAsync(user);
}
private async Task AnonymizeUserOrdersAsync(Guid userId)
{
var orders = await _orderRepository.GetListAsync(o => o.CustomerId == userId);
foreach (var order in orders)
{
order.ShippingAddress = "地址已刪除";
order.RecipientName = "已刪除的使用者";
order.RecipientPhone = null;
await _orderRepository.UpdateAsync(order);
}
}
}
}步驟 2:實作資料匯出服務
csharp
// Application/Users/UserDataExportService.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using BookStore.Orders;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
namespace BookStore.Users
{
public class UserDataExportService : ApplicationService
{
private readonly IRepository<AppUser, Guid> _appUserRepository;
private readonly IIdentityUserRepository _identityUserRepository;
private readonly IRepository<Order, Guid> _orderRepository;
public UserDataExportService(
IRepository<AppUser, Guid> appUserRepository,
IIdentityUserRepository identityUserRepository,
IRepository<Order, Guid> orderRepository)
{
_appUserRepository = appUserRepository;
_identityUserRepository = identityUserRepository;
_orderRepository = orderRepository;
}
public async Task<byte[]> ExportUserDataAsync(Guid userId)
{
var data = new Dictionary<string, object>();
// 1. 匯出基本資料
var identityUser = await _identityUserRepository.GetAsync(userId);
data["BasicInfo"] = new
{
identityUser.UserName,
identityUser.Email,
identityUser.PhoneNumber,
identityUser.Name,
identityUser.Surname,
identityUser.CreationTime
};
// 2. 匯出擴展資料
var appUser = await _appUserRepository.FirstOrDefaultAsync(u => u.IdentityUserId == userId);
if (appUser != null)
{
data["ExtendedInfo"] = new
{
appUser.PhoneNumber,
// 注意:敏感資料(如身分證)不應匯出,或需要額外驗證
CreationTime = appUser.CreationTime
};
}
// 3. 匯出訂單歷史
var orders = await _orderRepository.GetListAsync(o => o.CustomerId == userId);
data["Orders"] = orders.Select(o => new
{
o.OrderNumber,
o.TotalAmount,
o.Status,
o.CreationTime,
o.ShippingAddress
}).ToList();
// 4. 轉換為 JSON
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
return Encoding.UTF8.GetBytes(json);
}
}
}步驟 3:建立 Application Service 介面
csharp
// Application.Contracts/Users/IUserDataManagementAppService.cs
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace BookStore.Users
{
public interface IUserDataManagementAppService : IApplicationService
{
Task RequestDataDeletionAsync();
Task<byte[]> ExportMyDataAsync();
}
}csharp
// Application/Users/UserDataManagementAppService.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
namespace BookStore.Users
{
[Authorize]
public class UserDataManagementAppService : ApplicationService, IUserDataManagementAppService
{
private readonly UserDataDeletionService _deletionService;
private readonly UserDataExportService _exportService;
public UserDataManagementAppService(
UserDataDeletionService deletionService,
UserDataExportService exportService)
{
_deletionService = deletionService;
_exportService = exportService;
}
public async Task RequestDataDeletionAsync()
{
// 在實際應用中,這應該建立一個待處理的請求
// 由管理員審核後才執行刪除
await _deletionService.DeleteUserDataAsync(CurrentUser.Id.Value);
}
public async Task<byte[]> ExportMyDataAsync()
{
return await _exportService.ExportUserDataAsync(CurrentUser.Id.Value);
}
}
}步驟 4:建立 UI(Razor Pages 範例)
html
<!-- Pages/Account/MyData.cshtml -->
@page
@model MyDataModel
@inject IStringLocalizer<BookStoreResource> L
<h2>@L["MyPersonalData"]</h2>
<div class="card">
<div class="card-body">
<h5>@L["DataExport"]</h5>
<p>@L["DataExportDescription"]</p>
<form method="post" asp-page-handler="Export">
<button type="submit" class="btn btn-primary">
<i class="fas fa-download"></i> @L["ExportMyData"]
</button>
</form>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h5 class="text-danger">@L["DataDeletion"]</h5>
<p>@L["DataDeletionWarning"]</p>
<form method="post" asp-page-handler="Delete" onsubmit="return confirm('@L["DataDeletionConfirmation"]');">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i> @L["DeleteMyData"]
</button>
</form>
</div>
</div>csharp
// Pages/Account/MyData.cshtml.cs
using System.Threading.Tasks;
using BookStore.Users;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BookStore.Web.Pages.Account
{
public class MyDataModel : PageModel
{
private readonly IUserDataManagementAppService _userDataManagement;
public MyDataModel(IUserDataManagementAppService userDataManagement)
{
_userDataManagement = userDataManagement;
}
public async Task<IActionResult> OnPostExportAsync()
{
var data = await _userDataManagement.ExportMyDataAsync();
return File(data, "application/json", "my-data.json");
}
public async Task<IActionResult> OnPostDeleteAsync()
{
await _userDataManagement.RequestDataDeletionAsync();
return RedirectToPage("/Account/Logout");
}
}
}總結
本章練習涵蓋了安全性與資料保護的核心實作:
權限系統:
- 定義完整的權限樹結構
- 為不同角色分配適當的權限
- 在應用服務中強制權限檢查
- 測試未授權存取被正確拒絕
資料加密:
- 使用 ASP.NET Core Data Protection API
- 實作自動加密的 Value Converter
- 在資料庫層面保護敏感資料
- 驗證加密效果
GDPR 合規:
- 實作資料刪除(Right to Erasure)
- 實作資料匯出(Right to Data Portability)
- 資料匿名化而非直接刪除(保留業務記錄)
- 提供使用者友善的 UI
最佳實踐:
- 使用基於權限的授權而非基於角色
- 敏感資料必須加密儲存
- 實作完整的審計日誌
- 遵循 GDPR 和其他資料保護法規
- 定期進行安全審查和滲透測試