Writing in the front

In the last article, we briefly introduced shiro to a simple example. In this article, we will look at Shiro’s certification process by reading the source code.

It is recommended that you read the article while debugging the code, so that the effect is better.

Authentication Exception Analysis

Exceptions in Shiro are mainly divided into two categories: AuthenticationException and AuthorizationException. They correspond to 401 and 403 in the HTTP response status code

AuthenticationException subclass

Permission exception AuthorizationException subclass

An AuthenticationException subclass is thrown if authentication fails, and an AuthorizationException subclass is thrown if authentication fails.

We verify whether Shiro throws an exception to determine whether the logged object is authenticated and has access to the relevant protected resources.

This is why we needed to catch the related exception in the example in the previous section.

Next, let’s analyze the shiro framework certification process by reading the source code.

Analysis of certification Process

/** Authenticator *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/21 0:50 * /
public class Authenticator {

    private DefaultSecurityManager securityManager;

    public Authenticator(a){
        1. Create a security manager
        this.securityManager = new DefaultSecurityManager();

        //2. Set the problem domain for the security manager
        // Because permission information is read from the INI file, it is IniRealm
        this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

        //3. Inject the security manager and use the SecurityUtils global security tool class to complete authentication
        SecurityUtils.setSecurityManager(securityManager);



    }

    /**认证
     * @authorLaifeng [email protected] *@dateThe 2020-09-23 16:22:11 *@paramUsername username *@param"Password," password *@return void
     * @version1.0 * /
    public void authenticate(String username,String password){
        //4. Get the current topic
        Subject subject = SecurityUtils.getSubject();

        //5. Create a login token based on the identity certificate information of the login object
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        / / 6. Certification
        If the authentication passes, no exception is thrown; otherwise, the AuthenticationExceptixon exception subclass is thrown
        // It is recommended to throw a formal project directly to handle exceptions
        try {
            subject.login(token);
        }catch (IncorrectCredentialsException e) {
            e.printStackTrace();
        }catch (ConcurrentAccessException e){
            e.printStackTrace();
        }catch (UnknownAccountException e){
            e.printStackTrace();
        }catch (ExcessiveAttemptsException e){
            e.printStackTrace();
        }catch (ExpiredCredentialsException e){
            e.printStackTrace();
        }catch(LockedAccountException e){ e.printStackTrace(); }}}Copy the code

This is the code for the authenticator from the previous example. We make a breakpoint on line 44 of the above code, at the entrance to Shiro authentication, to track its authentication process.

Then start the program in the form of DEBUG in IDEA.

DelegatingSubject

The login () method

We find that we’re inside the DelegatingSubject.login method;

public class DelegatingSubject implements Subject {
    // Omit other code that does not affect understanding
      public void login(AuthenticationToken token) throws AuthenticationException {
         
        this.clearRunAsIdentitiesInternal();
          // 1. The real authentication is the securityManager object
        Subject subject = this.securityManager.login(this, token);
        String host = null;
        PrincipalCollection principals;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject)subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if(principals ! =null && !principals.isEmpty()) {
            this.principals = principals;
            this.authenticated = true;
            if (token instanceof HostAuthenticationToken) {
                host = ((HostAuthenticationToken)token).getHost();
            }

            if(host ! =null) {
                this.host = host;
            }

            Session session = subject.getSession(false);
            if(session ! =null) {
                this.session = this.decorate(session);
            } else {
                this.session = null; }}else {
            String msg = "Principals returned from securityManager.login( token ) returned a null or empty value.  This value must be non null and populated with one or more elements.";
            throw newIllegalStateException(msg); }}}Copy the code

From the source code above, we can see that although we call the authentication method of the Subject object, the real authentication is performed by the securityManager object, securityManager.

DefaultSecurityManager

The login () method

Next, we go to the Login method of the securityManager.

public class DefaultSecurityManager extends SessionsSecurityManager {


    // Omit other extraneous code
    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            // Call the authentication method
            info = this.authenticate(token);
        } catch (AuthenticationException var7) {
            AuthenticationException ae = var7;

            try {
                this.onFailedLogin(token, ae, subject);
            } catch (Exception var6) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6); }}throw var7;
        }

        Subject loggedIn = this.createSubject(token, info, subject);
        this.onSuccessfulLogin(token, info, loggedIn);
        returnloggedIn; }}Copy the code

AuthenticatingSecurityManager

The authenticate () method

When we enter to authenticate method, found him to be AuthenticatingSecurityManager method

public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
// Omit other extraneous code
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token); }}Copy the code

AbstractAuthenticator

The authenticate () method

Next, he calls the Authenticate method on the Authenticator object

public abstract class AbstractAuthenticator implements Authenticator.LogoutAware {
    // omit other extraneous methods
     public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        } else {
            log.trace("Authentication attempt received for token [{}]", token);

            AuthenticationInfo info;
            try {
              
                info = this.doAuthenticate(token);
                if (info == null) {
                    String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance. Please check that it is configured correctly.";
                    throw newAuthenticationException(msg); }}catch (Throwable var8) {
                AuthenticationException ae = null;
                if (var8 instanceof AuthenticationException) {
                    ae = (AuthenticationException)var8;
                }

                if (ae == null) {
                    String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException).";
                    ae = new AuthenticationException(msg, var8);
                    if(log.isWarnEnabled()) { log.warn(msg, var8); }}try {
                    this.notifyFailure(token, ae);
                } catch (Throwable var7) {
                    if (log.isWarnEnabled()) {
                        String msg = "Unable to send notification for failed authentication attempt - listener error? . Please check your AuthenticationListener implementation(s). Logging sending exception and propagating original AuthenticationException instead..."; log.warn(msg, var7); }}throw ae;
            }

            log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
            this.notifySuccess(token, info);
            returninfo; }}}Copy the code

ModularRealmAuthenticator

DoAuthenticate () method

And then into the ModularRealmAuthenticator certifier doAuthenticate method of objects

public class ModularRealmAuthenticator extends AbstractAuthenticator {
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        return realms.size() == 1 ? 
 /** Finally to the real authentication logic */            	this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); }}Copy the code

In this step, after test our Realms object creation, begin to enter into operation doSingleRealmAuthentication method of authentication

DoSingleRealmAuthentication () method

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if(! realm.supports(token)) { String msg ="Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type.";
            throw new UnsupportedTokenException(msg);
        } else {
            // Obtain the authentication information
            AuthenticationInfo info = realm.getAuthenticationInfo(token);
            if (info == null) {
                String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
                throw new UnknownAccountException(msg);
            } else {
                returninfo; }}}Copy the code

AuthenticatingRealm

GetAuthenticationInfo () method

In this step, we start getting authentication information based on the token we passed in

public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // Get it from the cache first
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            // If not in the cache, fetch from persistent data
            info = this.doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if(token ! =null&& info ! =null) {
                this.cacheAuthenticationInfoIfPossible(token, info); }}else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if(info ! =null) {
            this.assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
        }

        returninfo; }}Copy the code

SimpleAccountRealm

DoGetAuthenticationInfo () method

Get login object information from persistent data sources

public class SimpleAccountRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    UsernamePasswordToken upToken = (UsernamePasswordToken)token;
    // Query account information based on the user name
    SimpleAccount account = this.getUser(upToken.getUsername());
    // If the account information is found
    if(account ! =null) {
        // Start checking account status
        if (account.isLocked()) {
            throw new LockedAccountException("Account [" + account + "] is locked.");
        }

        if (account.isCredentialsExpired()) {
            String msg = "The credentials for account [" + account + "] are expired";
            throw newExpiredCredentialsException(msg); }}returnaccount; }}Copy the code

At this point, the validation of the user name is complete.

AuthenticatingRealm

Next, we get the account information and return to the getAuthenticationInfo method of AuthenticatingRealm.

In this method there are the following lines of code, and in the second line, the assertCredentialsMatch method is called to begin validating the user’s credentials

if(info ! =null) {
    this.assertCredentialsMatch(token, info);
} else {
    log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
Copy the code

AssertCredentialsMatch () method

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    // Get the credential matcher object
    CredentialsMatcher cm = this.getCredentialsMatcher();
    if(cm ! =null) {
        if(! cm.doCredentialsMatch(token, info)) { String msg ="Submitted credentials for token [" + token + "] did not match the expected credentials.";
            throw newIncorrectCredentialsException(msg); }}else {
        throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); }}Copy the code

** Complete verification of user credentials here. ** The real comparison logic is done in SimpleCredentialsMatcher’s equals method. Inside will also distinguish encryption and not encryption, please see the source code.

SimpleAccountRealm extends the AuthorizingRealm class to doGetAuthenticationInfo, and returns the result to AuthorizingRealm. AuthorizingRealm verifies user credentials for SimpleAccountRealm.

So, if we need to retrieve account information from the database, how do we pass the account information to Shiro for verification? I’ll leave that for you to think about, and I’ll answer it in the next article.

Write in the last

In this article, we clarified Shiro’s authentication process by debugging breakpoints and reading the source code. We unwrap it and find that the account authentication is done in the doGetAuthenticationInfo method of the SimpleAccountRealm object, Verification of user credentials is completed in assertCredentialsMatch in AuthenticatingRealm.

In the next article, we’ll learn how to use database information for authentication and authorization.