I want SMS login, email login and QQ login. What should I do?

@[toc] today I want to talk to you about the multi Realm authentication strategy in Shiro ~

In the project, if we want multiple login methods such as mobile phone verification code login, third-party QQ login and email login to coexist, we can consider implementing them through multiple realms in Shiro. In specific operations, one Realm just corresponds to one login method.

The usage of multi Realm login is not difficult. SongGe has previously sent relevant articles to share with you. Portal:

Today I don't want to talk about usage. I mainly want to talk about the relevant source code here. Therefore, this article requires you to have some experience in Shiro. If not, you can refer to the above link.

1. ModularRealmAuthenticator

1.1 where's realm?

The Realm we configured can be configured directly to the SecurityManager or to the ModularRealmAuthenticator in the SecurityManager.

If we are directly configured to the SecurityManager, after configuring the realms, we will automatically call the afterRealmsSet method. In this method, we will finally configure all the realms we configured to the ModularRealmAuthenticator.

The relevant source code is as follows:

RealmSecurityManager#setRealm (RealmSecurityManager is the parent of DefaultWebSecurityManager)

public void setRealm(Realm realm) {
    if (realm == null) {
        throw new IllegalArgumentException("Realm argument cannot be null");
    }
    Collection<realm> realms = new ArrayList<realm>(1);
    realms.add(realm);
    setRealms(realms);
}
public void setRealms(Collection<realm> realms) {
    if (realms == null) {
        throw new IllegalArgumentException("Realms collection argument cannot be null.");
    }
    if (realms.isEmpty()) {
        throw new IllegalArgumentException("Realms collection argument cannot be empty.");
    }
    this.realms = realms;
    afterRealmsSet();
}

You can see that whether you set a single Realm or multiple realms, you will eventually call the afterRealmsSet method, which is overridden in the authoringsecuritymanager#afterRealmsSet class. The contents are as follows:

protected void afterRealmsSet() {
    super.afterRealmsSet();
    if (this.authorizer instanceof ModularRealmAuthorizer) {
        ((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms());
    }
}

As you can see, all realms are finally set to ModularRealmAuthenticator.

Therefore, whether it is a single Realm or multiple realms, they are finally managed and called uniformly by ModularRealmAuthenticator.

1.2 how to play modularrealmauthenticator

The core method in ModularRealmAuthenticator is doAuthenticate, as follows:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

The logic of this method is simple:

  1. First, call the assertRealmsConfigured method to determine whether the developer has configured real. If not, the exception will be thrown directly.
  2. Judge how many realms are configured by the developer. If one is configured, call the dosinglerealauthentication method for processing. If multiple realms are configured, call the domultirealauthentication method for processing.

Configuring one Realm is relatively simple, which is beyond the scope of this article. This article mainly wants to discuss the situation of multiple realms with you.

When there are multiple realms, it will inevitably bring another problem: authentication strategy, that is, how can authentication be successful? If one Realm authentication is successful, is it successful, or is it successful only if all realms are successful? Or something.

Next, let's talk about this topic in detail.

2. AuthenticationStrategy

On the whole, the class responsible for authentication policy is AuthenticationStrategy, which is an interface with three implementation classes:

Literally, the three implementation classes are easy to understand:

  • Atleastonesuccessful strategy: at least one Realm has been successfully authenticated.
  • All successful strategy: all realms must be authenticated successfully.
  • First successful strategy: This is not very accurate in literal terms. It only returns the data of the first user with successful authentication.

The second kind is actually very easy to understand. The problem is that the first and third ones can be understood separately. If they are put together, someone can't help asking, what's the difference between the two?

To be honest, the version before 1.3.2 is really no big difference, but there are still some differences in the latest version, and let brother song analyze it.

First of all, there are four methods involved:

  • Before all attempts: prepare before all realms are verified.
  • beforeAttempt: prepare for validation before a single Realm.
  • After attempt: handle subsequent matters after single Realm verification.
  • After all attempts: handle all subsequent matters after real verification.

The first and fourth methods are called only once in each authentication process, while the middle two methods are called before and after each Realm call. The pseudo code is similar to the following:

The above four methods have different implementations in the four implementation classes of AuthenticationStrategy. I have sorted out the following table for your understanding:

Please note that a merge method is added here. This method is defined in the AbstractAuthenticationStrategy class. When there are multiple realms, it is used to merge the authentication data in multiple realms. Next, we will analyze several methods here one by one according to the order of this table.

2.1 AbstractAuthenticationStrategy

2.1.1 beforeAllAttempts

Let's look at the code directly:

public AuthenticationInfo beforeAllAttempts(Collection<!--? extends Realm--> realms, AuthenticationToken token) throws AuthenticationException {
    return new SimpleAuthenticationInfo();
}

Nothing is done here, so an empty SimpleAuthenticationInfo object is created.

2.1.2 beforeAttempt

public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
    return aggregate;
}

The logic of this method is also very simple. The aggregate parameter passed in refers to the results aggregated after multiple realms are authenticated. Nothing is done here. The results are directly returned intact.

2.1.3 afterAttempt

public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
    AuthenticationInfo info;
    if (singleRealmInfo == null) {
        info = aggregateInfo;
    } else {
        if (aggregateInfo == null) {
            info = singleRealmInfo;
        } else {
            info = merge(singleRealmInfo, aggregateInfo);
        }
    }
    return info;
}

This is what to do after each Realm authentication is completed. The parameter singleRealmInfo represents the result of a single Realm authentication, and aggregateInfo represents the aggregation of multiple Realm authentication results. The specific logic is as follows:

  1. If the current real authentication result is null, the aggregation result is assigned to info and returned.
  2. If the current Realm authentication result is not null and the aggregation result is null, the current Realm authentication result is assigned to info and returned.
  3. If the current real authentication result is not null and the aggregation result is not null, the two are combined and returned.

2.1.4 afterAllAttempts

public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
    return aggregate;
}

Here, the aggregation result is returned directly. There's nothing to say.

2.1.5 merge

protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
    if( aggregate instanceof MergableAuthenticationInfo ) {
        ((MergableAuthenticationInfo)aggregate).merge(info);
        return aggregate;
    } else {
        throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate " +
                  "AuthenticationInfo is not of type MergableAuthenticationInfo." );
    }
}

Merge actually calls the merge method of aggregate to merge. Under normal circumstances, SimpleAuthenticationInfo we use is a subclass of mercableauthenticationinfo, so there is no problem with merging here.

2.2 AtLeastOneSuccessfulStrategy

2.2.1 beforeAllAttempts

Same as subsection 2.1.1.

2.2.2 beforeAttempt

The same as subsection 2.1.2.

2.2.3 afterAttempt

Same as subsection 2.1.3.

2.2.4 afterAllAttempts

public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
    //we know if one or more were able to successfully authenticate if the aggregated account object does not
    //contain null or empty data:
    if (aggregate == null || isEmpty(aggregate.getPrincipals())) {
        throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
                "could not be authenticated by any configured realms.  Please ensure that at least one realm can " +
                "authenticate these tokens.");
    }
    return aggregate;
}

The logic here is very clear, that is, when the aggregation result is empty, an exception will be thrown directly.

2.2.5 merge

Same as subsection 2.1.5.

2.2.6 summary

Combined with the contents of Section 2.1, let's sort out the functions of atleast one successful strategy.

  1. First, the system calls the beforeAllAttempts method to get an empty SimpleAuthenticationInfo object as the aggregate result aggregate.
  2. Next, traverse all realms and call the beforeAttempt method before each Realm call. This method will only return the aggregation result aggregate intact.
  3. The getAuthenticationInfo method of each Realm is called for authentication.
  4. Call the afterAttempt method to aggregate the authentication results. If the current Realm authentication returns null, the aggregation result is returned; If the current real authentication does not return null, merge the current real authentication result with aggregate (aggregate will not be null, because the beforeAllAttempts method returns an empty object).

This is the authentication strategy of atleast one successful strategy. You can see that if only one Realm is authenticated successfully, it will return normally. If multiple realms are authenticated successfully, the returned user information will contain multiple authenticated user information.

Multiple returned user information can be obtained in the following ways:

Subject subject = SecurityUtils.getSubject();
subject.login(token);
PrincipalCollection principals = subject.getPrincipals();
List list = principals.asList();
for (Object o : list) {
    System.out.println("o = " + o);
}

The subject.getPrincipals() method can obtain multiple credentials for successful authentication.

2.3 AllSuccessfulStrategy

2.3.1 beforeAllAttempts

Same as subsection 2.1.1.

2.3.2 beforeAttempt

public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    if (!realm.supports(token)) {
        String msg = "Realm [" + realm + "] of type [" + realm.getClass().getName() + "] does not support " +
                " the submitted AuthenticationToken [" + token + "].  The [" + getClass().getName() +
                "] implementation requires all configured realm(s) to support and be able to process the submitted " +
                "AuthenticationToken.";
        throw new UnsupportedTokenException(msg);
    }
    return info;
}

As you can see, here is to check whether the next Realm supports the current token.

I find this code a little strange. Why don't other authentication strategies be checked, only here? It feels like a BUG. A knowledgeable partner can leave a message to discuss this issue.

2.3.3 afterAttempt

public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
        throws AuthenticationException {
    if (t != null) {
        if (t instanceof AuthenticationException) {
            throw ((AuthenticationException) t);
        } else {
            String msg = "Unable to acquire account data from realm [" + realm + "].  The [" +
                    getClass().getName() + " implementation requires all configured realm(s) to operate successfully " +
                    "for a successful authentication.";
            throw new AuthenticationException(msg, t);
        }
    }
    if (info == null) {
        String msg = "Realm [" + realm + "] could not find any associated account data for the submitted " +
                "AuthenticationToken [" + token + "].  The [" + getClass().getName() + "] implementation requires " +
                "all configured realm(s) to acquire valid account data for a submitted token during the " +
                "log-in process.";
        throw new UnknownAccountException(msg);
    }
    merge(info, aggregate);
    return aggregate;
}

If the current authentication fails or the authentication result is null, an exception will be thrown directly (because each Realm is required to be authenticated successfully, but if one authentication fails, the subsequent authentication is not necessary).

If everything is OK, the results are merged and returned.

2.3.4 afterAllAttempts

The same as subsection 2.1.4.

2.3.5 merge

Same as subsection 2.1.5.

2.3.6 summary

This strategy is relatively simple. There should be no need to explain it. If multiple realms are authenticated successfully, the authentication information of multiple realms will also be returned here. Obtain the authentication information of multiple realms, as in the previous section.

2.4 FirstSuccessfulStrategy

2.4.1 beforeAllAttempts

public AuthenticationInfo beforeAllAttempts(Collection<!--? extends Realm--> realms, AuthenticationToken token) throws AuthenticationException {
    return null;
}

Unlike before, null is returned directly here.

2.4.2 beforeAttempt

public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
    if (getStopAfterFirstSuccess() &amp;&amp; aggregate != null &amp;&amp; !isEmpty(aggregate.getPrincipals())) {
        throw new ShortCircuitIterationException();
    }
    return aggregate;
}

The logic here is that if the getStopAfterFirstSuccess() method returns true and the aggregation of the current authentication result is not empty, an exception will be thrown directly. Once an exception is thrown, the current loop will jump out, that is, the current Realm will not be called for authentication. This idea is basically consistent with the name of first successful strategy.

However, there is a method getStopAfterFirstSuccess(). You can know whether to stop authentication after the first success by looking at the name. By default, this variable is false, that is, even after the first success, the authentication of the following real will continue.

If we hope that the subsequent realms will not be authenticated after the first authentication is successful, remember to configure this attribute to true.

2.4.3 afterAttempt

Same as subsection 2.1.3.

2.4.4 afterAllAttempts

The same as subsection 2.1.4.

2.4.5 merge

I don't know if the partners still remember where the merge method was called. Review section 2.1.3. If the authentication and aggregation results of the current real are not null, the results need to be merged. The original merging is a real merging. This method is rewritten here, so the merging is not performed:

protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
    if (aggregate != null &amp;&amp; !isEmpty(aggregate.getPrincipals())) {
        return aggregate;
    }
    return info != null ? info : aggregate;
}

This is the only one of the three strategies that overrides the merge method.

The merge here does not have a real merge, but:

  1. If the aggregation result is not empty, the aggregation result is returned directly.
  2. Otherwise, if the current authentication result is not empty, the current authentication result is returned.
  3. Otherwise, it returns null.

You can see that the merge here actually selects an authenticated info return. If there is a previously authenticated Realm, the info returned after the subsequent Realm is successfully authenticated will not be used.

2.4.6 summary

Well, now we can sum up the difference between first successful strategy and atleast one successful strategy:

  1. Atleastonesuccessful strategy: when there are multiple realms, even if one of the realms has been successfully authenticated, the subsequent realms will still be authenticated. If the subsequent realms have also been successfully authenticated, the results of successful authentication of multiple realms will be combined.
  2. First successful strategy: when there are multiple realms, by default, even if one of the realms has been successfully authenticated, the subsequent realms will still be authenticated. However, if the subsequent realms have been successfully authenticated, the results returned by the subsequent realms will not be used. If we hope that after a Realm is authenticated successfully, the subsequent realms will no longer be authenticated, we can configure the value of stopAfterFirstSuccess attribute as follows:
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
    <property name="authenticator">
        <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
            <property name="authenticationStrategy">
                <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy">
                    <property name="stopAfterFirstSuccess" value="true" />
                </bean>
            </property>
            <property name="realms">
                <list>
                    <ref bean="myRealm01" />
                    <ref bean="myRealm02" />
                </list>
            </property>
        </bean>
    </property>
</bean>

3. Summary

Well, this is the Shiro multi Realm situation that brother song shared with you. Interested partners can try it~

The official account is back to Shiro for Shiro. realm></realm></realm></realm>

Posted on Mon, 08 Nov 2021 07:01:07 -0500 by EagleAmerican