Mở rộng lớp AbpSession trong ABP Framework

Hiểu về cơ chế hoạt động của AbpSession

Phân biệt giữa AbpSession và Session truyền thống

Khi xem xét nguồn gốc của hai thành phần này, ta nhận thấy: Trong Controller, Session là thuộc tính kiểu HttpSessionStateBase:
public HttpSessionStateBase Session { get; set; }
Trong khi đó, AbpSession được định nghĩa trong AbpController như sau:
public IAbpSession AbpSession { get; set; }
Vì vậy, AbpSession thực chất là một đối tượng kiểu IAbpSession, không phải là phiên bản mở rộng của Session HTTP truyền thống.

Cơ chế triển khai cụ thể của IAbpSession

Trong ABP có hai cách triển khai cho IAbpSession:
  • NullAbpSession: Sử dụng mẫu thiết kế Null Object để xử lý trường hợp dependency injection không thành công
  • ClaimsAbpSession: Triển khai chính dựa trên hệ thống Claims

Phân tích lớp ClaimsAbpSession

Dưới đây là đoạn mã nguồn minh họa cách ClaimsAbpSession truy xuất thông tin người dùng:
public class ClaimsAbpSession : IAbpSession, ISingletonDependency
{
    public virtual long? UserId
    {
        get
        {
            var userClaim = PrincipalProvider.Principal?.FindFirst(ClaimTypes.NameIdentifier);
            if (string.IsNullOrWhiteSpace(userClaim?.Value))
            {
                return null;
            }

            if (long.TryParse(userClaim.Value, out long userId))
            {
                return userId;
            }

            return null;
        }
    }

    public IPrincipalProvider PrincipalProvider { get; set; }

    public ClaimsAbpSession(IMultiTenancyConfig multiTenancyConfig)
    {
        MultiTenancy = multiTenancyConfig;
        PrincipalProvider = DefaultPrincipalProvider.Instance;
    }
}
Lớp DefaultPrincipalProvider đóng vai trò cung cấp thông tin người dùng hiện tại:
public class DefaultPrincipalProvider : IPrincipalProvider, ISingletonDependency
{
    public virtual ClaimsPrincipal Principal => Thread.CurrentPrincipal as ClaimsPrincipal;
    
    public static DefaultPrincipalProvider Instance => new DefaultPrincipalProvider();
}
Từ đó có thể thấy rằng AbpSession lấy dữ liệu từ ClaimsPrincipal chứ không phụ thuộc vào HttpSession.

Giới thiệu về hệ thống xác thực Identity

Khái niệm Claims

Claims là các thông tin xác thực về danh tính người dùng, tương tự như các thông tin trên chứng minh thư: tên, giới tính, ngày sinh, địa chỉ, số CMND...

ClaimsIdentity và ClaimsPrincipal

ClaimsIdentity đại diện cho một danh tính cụ thể với tập hợp các claims:
public class ClaimsIdentity : IIdentity
{
    public virtual IEnumerable<Claim> Claims { get; }
    public virtual string Name { get; }
    public virtual string AuthenticationType { get; }
    
    public virtual void AddClaim(Claim claim);
    public virtual void RemoveClaim(Claim claim);
}
ClaimsPrincipal quản lý nhiều ClaimsIdentity:
public class ClaimsPrincipal : IPrincipal
{
    public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities);
    public virtual IIdentity Identity { get; }
    public virtual IEnumerable<ClaimsIdentity> Identities { get; }
    public virtual void AddIdentity(ClaimsIdentity identity);
}

Quy trình đăng nhập trong ABP

Trong AccountController, quy trình đăng nhập bao gồm:
[HttpPost]
public async Task<JsonResult> Authenticate(LoginInputDto inputModel)
{
    CheckModelState();

    var authResult = await ValidateCredentialsAsync(
        inputModel.UsernameOrEmail,
        inputModel.Password,
        inputModel.TenantName
    );

    await ProcessSignInAsync(authResult.User, authResult.Identity, inputModel.RememberMe);

    return Json(new AjaxResponse { TargetUrl = GetReturnUrl(inputModel.ReturnUrl) });
}

private async Task<AuthValidationResult<Tenant, User>> ValidateCredentialsAsync(
    string usernameOrEmail, 
    string password, 
    string tenantName)
{
    var result = await _authManager.ValidateAsync(usernameOrEmail, password, tenantName);

    switch (result.Status)
    {
        case AuthStatus.Valid:
            return result;
        default:
            throw CreateAuthFailureException(result.Status, usernameOrEmail, tenantName);
    }
}

private async Task ProcessSignInAsync(User user, ClaimsIdentity identity = null, bool rememberMe = false)
{
    if (identity == null)
    {
        identity = await _userManager.GenerateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    }

    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberMe }, identity);
}

Cách tiếp cận để mở rộng AbpSession

Để mở rộng AbpSession, cần thêm thông tin vào ClaimsIdentity trong quá trình đăng nhập.

Phương pháp 1: Sử dụng phương thức mở rộng (đề xuất)

Bước 1: Thêm thông tin vào Claims trong quá trình đăng nhập:

// Trong phương thức ProcessSignInAsync
identity.AddClaim(new Claim(ClaimTypes.Email, user.EmailAddress));
identity.AddClaim(new Claim("CustomProperty", customValue));

Bước 2: Tạo lớp mở rộng cho IAbpSession:

namespace YourProject.Extensions
{
    public static class ExtendedAbpSessionExtensions
    {
        public static string GetUserEmail(this IAbpSession session)
        {
            return ExtractClaimValue(ClaimTypes.Email);
        }

        public static string GetCustomProperty(this IAbpSession session)
        {
            return ExtractClaimValue("CustomProperty");
        }

        private static string ExtractClaimValue(string claimType)
        {
            var principal = DefaultPrincipalProvider.Instance.Principal;
            var claim = principal?.FindFirst(claimType);
            return claim?.Value;
        }
    }
}
Phương pháp này đơn giản và hiệu quả, không yêu cầu thay đổi cấu trúc dependency injection.

Phương pháp 2: Ghi đè toàn bộ IAbpSession

Bước 1: Định nghĩa giao diện mở rộng:

namespace YourProject.Extensions
{
    public interface IExtendedAbpSession : IAbpSession
    {
        string Email { get; }
        string CustomData { get; }
    }
}

Bước 2: Triển khai lớp mở rộng:

namespace YourProject.Extensions
{
    public class ExtendedAbpSession : ClaimsAbpSession, IExtendedAbpSession
    {
        public ExtendedAbpSession(IMultiTenancyConfig multiTenancyConfig) 
            : base(multiTenancyConfig)
        {
        }

        public string Email => GetClaimValue(ClaimTypes.Email);
        
        public string CustomData => GetClaimValue("CustomProperty");

        private string GetClaimValue(string claimType)
        {
            var principal = PrincipalProvider.Principal;
            var claim = principal?.FindFirst(claimType);
            return claim?.Value;
        }
    }
}

Bước 3: Ghi đè trong các lớp cơ sở:

// Trong ControllerBase
public new IExtendedAbpSession AbpSession { get; set; }

// Trong ApplicationServiceBase  
public new IExtendedAbpSession AbpSession { get; set; }
Cách tiếp cận thứ hai phức tạp hơn nhưng cung cấp khả năng kiểm soát tốt hơn.

Kết luận

AbpSession hoạt động dựa trên hệ thống Claims của .NET, không liên quan đến HttpSession truyền thống. Để mở rộng chức năng, cần thêm thông tin vào Claims trong quá trình xác thực và trích xuất thông tin đó qua hệ thống ClaimsPrincipal.

Thẻ: abp-framework session-management claims-based-authentication aspnet-core authentication

Đăng vào ngày 2 tháng 10 lúc 13:46