ABP Introduction Series (10) - Extending AbpSession

Keywords: ASP.NET Session github Attribute

ABP Introductory Series Catalogue: Practical Exercise on Learning Abp Framework
Source path: Github-Learning MpaAbp

Is AbpSession Session?

1. Let's first look at their respective types.

Looking at the source code, it is found that Session is a property defined in Controller of type HttpSessionStateBase.
public HttpSessionStateBase Session { get; set; }

Let's look at what kind of AbpSession is. Let's locate it in AbpController.
public IAbpSession AbpSession { get; set; }

Well, AbpSession is IAbpSession type. But can we conclude that AbpSession is not Session?
Not necessarily, if IAbpsession is still dependent on Session in its implementation, then AbpSession can be regarded as an extension of Session, or Session.
Let's look for the specific implementation of IAbpsession.
There are two implementations of IAbpsession in Abp. One is NullAbpSession, which is an empty object design pattern and is initialized in the constructor for attribute injection.
The other is Claims Abp Session. Let's explore it.

2. Claims AbpSession

The following code is an excerpt from Claims AbpSession:

/// <summary>
/// Implements <see cref="IAbpSession"/> to get session properties from claims of <see cref="Thread.CurrentPrincipal"/>.
/// </summary>
public class ClaimsAbpSession : IAbpSession, ISingletonDependency
{
    public virtual long? UserId
    {
        get
        {
            var userIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
            if (string.IsNullOrEmpty(userIdClaim?.Value))
            {
                return null;
            }

            long userId;
            if (!long.TryParse(userIdClaim.Value, out userId))
            {
                return null;
            }

            return userId;
        }
    }

    public IPrincipalAccessor PrincipalAccessor { get; set; }

    public ClaimsAbpSession(IMultiTenancyConfig multiTenancy)
    {
        MultiTenancy = multiTenancy;
        PrincipalAccessor = DefaultPrincipalAccessor.Instance;
    }
}

What is the devil of IPrincipal Accessor? From the constructor point of view, Default Principal Accessor should be a singleton mode.

public class DefaultPrincipalAccessor : IPrincipalAccessor, ISingletonDependency
{
   public virtual ClaimsPrincipal Principal => Thread.CurrentPrincipal as ClaimsPrincipal;
   public static DefaultPrincipalAccessor Instance => new DefaultPrincipalAccessor();
}

By neutralizing the above two parts of code, the UserId in AbpSession is not obtained in this way:
((ClaimsPrincipal)Thread.CurrentPrincipal).Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);

Well, at a glance, AbpSession ultimately relies on Claims Principal, not Session.

So AbpSession is not Session!!!
So AbpSession is not Session!!!
So AbpSession is not Session!!!

What the hell is Claims Principal? I just like your power to break through the casserole and listen to me.

Identity authentication

This section mainly refers to self-blogging Park Savorboard I would like to thank Savorboard for sharing this blog. I suggest you read it carefully.

Introduction to Identity of ASP.NET Core (I)
Introduction to Identity of ASP.NET Core (II)
Introduction to Identity of ASP.NET Core (3)

Cliam (identity information)

Take the ID card for example, including name: Obama, gender: man, nationality: xx, birth: xx, address: xx, citizen province number: xxx, these key pairs are identity information. Name, gender, nationality, birth, address, citizen province number are Claims Type. Microsoft has defined a series of categories of identity information for us, including Email, Gender, Phone and so on.

2. Claims Identity

With identity information, a set of packages will not become identity cards.
Look at Claims Identity's brief code:

public class ClaimsIdentity: IIdentity
{
    public virtual IEnumerable<Claim> Claims
    {
    get {   //Eliminate other code}
    }
    
    //Names are so important, of course, they can't be changed at will, so I don't allow set, except for my son and my surname, so it's virtual.
    public virtual string Name { get; }
    
    //This is my type of certificate. It's also very important. No set is allowed.
    public virtual string AuthenticationType { get; }
    
    public virtual void AddClaim(Claim claim);
    
    public virtual void RemoveClaim(Claim claim);
    
    public virtual void FindClaim(Claim claim);
}

You can see that Claims Identity maintains a list of Claim enumerations.
The Authentication Type, literally, is the authentication type. What does that mean? For example, when we take ID cards to government departments for business, sometimes we need to hold our ID cards, but sometimes we need a copy of the ID cards.

3. Claims Principal

We use identity information to construct an identity card, which must belong to a specific person.
So Claims Principal is used to maintain a stack of documents.
Because the same is true in real life, we have a series of ID cards, bank cards, social security cards and other documents.
Let's see how it is realized in. net.

//Core Code Section
public class ClaimsPrincipal :IPrincipal
{
    //Give all the documents to the parties concerned
    public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities){}
    
    //What about the principal status of the parties?
    public virtual IIdentity Identity { get; }
    
    public virtual IEnumerable<ClaimsIdentity> Identities { get; }
    
    public virtual void AddIdentity(ClaimsIdentity identity);
    
    //Why is there no RemoveIdentity left for you to think about?
}

With these concepts in mind, let's take a look at Identity's brief login process:

From this picture, we provide some identity information Claim (username/password) when we log in, and then the Identity middleware constructs an identity card Claims Identity based on the identity information, and then gives the identity card to the owner of Claims Principal document for safekeeping.

3. Stroke the landing process in Abp

Locate AccountController and focus on the following code:

[HttpPost]
[DisableAuditing]
public async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "")
{
    CheckModelState();

    var loginResult = await GetLoginResultAsync(
        loginModel.UsernameOrEmailAddress,
        loginModel.Password,
        loginModel.TenancyName
        );

    await SignInAsync(loginResult.User, loginResult.Identity, loginModel.RememberMe);

    if (string.IsNullOrWhiteSpace(returnUrl))
    {
        returnUrl = Request.ApplicationPath;
    }

    if (!string.IsNullOrWhiteSpace(returnUrlHash))
    {
        returnUrl = returnUrl + returnUrlHash;
    }

    return Json(new AjaxResponse { TargetUrl = returnUrl });
}

private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName)
{
    var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);

    switch (loginResult.Result)
    {
        case AbpLoginResultType.Success:
            return loginResult;
        default:
            throw CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName);
    }
}

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

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

The analysis discovery mainly includes the following steps:
1. GetLoginResultAsync - > LoginManager. LoginAsync - > userManager. CreateIdentity Async: Don't assume that calling LoginAsync is login, but it's pseudologin. The user information is checked mainly according to the user name and password, and the User object is constructed to return. Then the Cliams Identity is constructed according to the identity information of the User object.
2,SignInAsync --> AuthenticationManager.SignOut
-->AuthenticationManager.SignIn :
Authentication Manager (Authentication Manager), responsible for real login and login. SignIn gives the Cliams Identity constructed in the first step to the Claims Principal.

Do you understand how to extend AbpSession?
The key is to add identity information to the Cliam s Identity!!!

In fact, go to the Abp official website on github to search for issue s, and find that Turkish Daniel is also giving this kind of extension ideas, for more information. This chain.

IV. Start expanding AbpSession

The last section has cleared the way of thinking. Let's roll up our sleeves and expand in this section.
AbpSession attributes have been injected into three base classes: Application Service, AbpController and ABP ApiController.
So we need to extend AbpSession at the domain level, which is the project at the end of. Core.
Now suppose we need to extend an Email attribute.

1. Extending IAbpSession

Locate the project at the end of. Core, add the Extensions folder, and then add the IAbpSession Extension interface inherited from IAbpSession:

namespace LearningMpaAbp.Extensions
{
    public interface IAbpSessionExtension : IAbpSession
    {
        string Email { get; }
    }
}

2. Implementing IAbpSession Extension

Add the AbpSession Extension class, which is based on Claims AbpSession and implements the IAbpSession Extension interface.

namespace LearningMpaAbp.Extensions
{
    public class AbpSessionExtension : ClaimsAbpSession, IAbpSessionExtension
    {
        public AbpSessionExtension(IMultiTenancyConfig multiTenancy) : base(multiTenancy)
        {
        }

        public string Email => GetClaimValue(ClaimTypes.Email);

        private string GetClaimValue(string claimType)
        {
            var claimsPrincipal = PrincipalAccessor.Principal;

            var claim = claimsPrincipal?.Claims.FirstOrDefault(c => c.Type == claimType);
            if (string.IsNullOrEmpty(claim?.Value))
                return null;

            return claim.Value;
        }
    }
}

3. Replace the injected AbbSession attribute

First replace the injected ABP Session in AbpController
Locate. Web Controllers xxxController Base. CS and inject IAbpSession Extension with attributes. Add the following code:

//AbpSession Hiding Parent Class
public new IAbpSessionExtension AbpSession { get; set; }

Replace the injected ABP Session in Application Service
Locate. ApplicationxxxAppServiceBase.cs. Introduce IAbpSession Extension with attributes, and add the following code as well:

//AbpSession Hiding Parent Class
public new IAbpSessionExtension AbpSession { get; set; }

Whether or not AbpApiController wants to replace AbpSession depends on the situation. If you use Abp's dynamic Web Api technology, you don't need to replace it, because after all, the Api of the application service layer is ultimately invoked. If WebApi is implemented in your own code, replace it by copying it, and it's not too verbose.

4. Add Cliam (Identity Information) before login

Locate the AccountController, modify the SignInAsync method, and add the following code before calling Authentication Manager. SignIn:
identity.AddClaim(new Claim(ClaimTypes.Email, user.EmailAddress));

5. No plan, no truth

Referring to the following blog posts, I would like to thank them again for their wonderful sharing.
Introduction to Identity of ASP.NET Core (1) -- Savorboard
Introduction to Identity of ASP.NET Core (2) -- Savorboard
Introduction to Identity of ASP.NET Core (3) -- Savorboard
The AbpSession Extension of Asp.net Boilerplate--kid1412
DDD-based. NET Development Framework-ABP Session Implementation-Joye.Net

Posted by jurasiprize on Fri, 22 Mar 2019 12:27:54 -0700