【shiro权限管理】6.shiro认证的代码思路

后面的部分源码进行了省略,只贴出跟认证主线流程相关代码!

shiro是如何做认证的呢?首先回顾一下之前剖析的Shiro的HelloWorld程序中有关认证的部分代码:

//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
//测试当前用户是否已经被认证(即是否已经登录)
if (!currentUser.isAuthenticated()) {
    //将用户名与密码封装为UsernamePasswordToken对象
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    token.setRememberMe(true);//记录用户
   try {
        currentUser.login(token);//调用Subject的login方法执行登录
    } catch (UnknownAccountException uae) {
        log.info("There is no user with username of " + token.getPrincipal());
    } catch (IncorrectCredentialsException ice) {
        log.info("Password for account " + token.getPrincipal() + " was incorrect!");
    } catch (LockedAccountException lae) {
       log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                "Please contact your administrator to unlock it.");
    }
}

在上面的代码中,首先获取当前用户的Subject对象,然后通过Subject的isAuthenticated判断用户是否已经登录。如果没有登录,就进行认证,这里开发者需要做的就两点:

1.构建AuthenticationToken实例,一般项目中大都用用户名密码方式校验,则根据前台传的用户名密码构建UsernamePasswordToken

2.调用Subject实例的login方法进行验证

可以想见认证过程都是在这个login方法里进行的,它的内部主要做了两点:

1.获取后台用户信息

2.将前后台获取的用户信息进行比对,验证是否通过

回顾一下之前的架构:

【shiro权限管理】6.shiro认证的代码思路
 

注意其中的Realm,在Shiro的架构中,负责和数据库交互的对象就是Realm对象。这里先点一下,当校验账号与密码时,由Realm提供用户信息,这个由开发者负责实现,而比较密码的工作是Shiro来帮我们完成的

 一、获取后台用户信息

那currentUser.login(token);这句代码是如何做验证的,以前面HelloWorld的代码为例:

上面当前用户的Subject的实现类DelegatingSubject的login方法如下:

public void login(AuthenticationToken token) throws AuthenticationException {
   //方法比较长,只要关注这一行就行,可见login是由SecurityManager完成的
    Subject subject = securityManager.login(this, token);
}

DefaultSecurityManager的login中:

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            //调用父类的authenticate方法
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            //捕获认证异常做相应处理后再抛出异常,看HelloWorld的代码可以发现在登录相关代码中
           //如果登录失败是根据异常做相关处理的,也就是说只要认证失败就会抛出异常这个很重要,
            throw ae; 
        }
        Subject loggedIn = createSubject(token, info, subject);
        onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

DefaultSecurityManager父类AuthenticatingSecurityManager的authenticate方法:

public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    //可以看到认证最后是交给SecurityManager内部维护的认证器authenticator来实现的
    return this.authenticator.authenticate(token);
}

本例中的认证器是ModularRealmAuthenticator,执行的是他父类AbstractAuthenticator的authenticate方法:

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        }
        log.trace("Authentication attempt received for token [{}]", token);
        AuthenticationInfo info;
        try {
          //实际执行的是doAutenticate方法
            info = doAuthenticate(token);
            if (info == null) {
                String msg = "";
                throw new AuthenticationException(msg);
            }
        } catch (Throwable t) {
            AuthenticationException ae = null;
            if (t instanceof AuthenticationException) {
                ae = (AuthenticationException) t;
            }
            if (ae == null) {
                String msg = "";
                ae = new AuthenticationException(msg, t);
                if (log.isWarnEnabled())
                    log.warn(msg, t);
            }
            try {
                notifyFailure(token, ae);
            } catch (Throwable t2) {
                if (log.isWarnEnabled()) {
                    String msg = "";
                    log.warn(msg, t2);
                }
            }
           //如上所言,如果内部方法doAuthenticate方法报错(也是因为认证不通过)这里将抛出AuthenticationException
           //shiro就是通过这样层层抛出错误的方法来确定认证结果
            throw ae;
        }
        notifySuccess(token, info);
        return info;
    }

doAuthenticate执行的是ModularRealmAuthnticator的实现方法:

public AuthenticationInfo doAuthenticate(AuthenticationToken token) throws AuthenticationException {
    assertRealmsConfidured();
    Collection<Realm> realms = getRealms();
    if(realms.size() == 1 ){
        return doSingleRealmAuthentication(realms,iterator().next,authenticationToken);
    }else{
        return doMutiRealmAuthentication(realms,authenticationToken);
    }
}

可以看到,在doAuthenticate方法中,首先会获取所有的Realm,然后根据Realm的数量来决定使用单个Realm的校验方法,还是多个Realm的校验方法。我们以配置单个Realm的情况为例doSingleRealmAuthentication方法如下:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
       //如上这里认证不通过也是抛出异常 
       if (!realm.supports(token)) {
            String msg = "";
            throw new UnsupportedTokenException(msg);
        }
      //实际调用配置的realm中的getAuthenticationInfo方法
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            String msg = "";
            throw new UnknownAccountException(msg);
        }
        return info;
    }

realm实例类AuthenticatingRealm的getAuthenticationInfo方法如下:

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       //获取缓存的认证信息这里不管
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //实际查找认证信息是这个方法
            info = doGetAuthenticationInfo(token);
        } else {
        }
        if (info != null) {
           //这里验证密码后面会提到
            assertCredentialsMatch(token, info);
        } else {
        }
        return info;
    }

在HelloWorld例子中用的ini配置文件,所以自动配置了简单的SimpleAccountRealm,它的doGetAuthenticationInfo方法:

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
       //这里的SimpleAccount实现了AuthenticationInfo接口
        SimpleAccount account = getUser(upToken.getUsername());
        if (account != null) {
            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }
        return account;
    }

 到这里我们就跟开发者需要做的另外一件事对接上了,那就是实现Realm接口,包括它的doGetAuthenticationInfo方法,我们实现这个方法要做的就是如果找到认证信息则返回,如果找不到或者账号被锁住则抛出相应异常如上面提到的

二、密码验证

到这里login方法内部有了前台传过来的用户名密码,以及从Realm实现类中获取的用户信息,还缺一步就是两个信息的比对,也就是密码验证。这一步在哪实现呢?

往上找AuthenticatingRealm这个类,也就是一般我们实现Realm都会继承的抽象类,它里面的assertCredentialsMatch方法:

//此方法没有返回值比对通过方法结束,否则抛出异常
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
           //doCredentialsMatch就是比对密码的地方
            if (!cm.doCredentialsMatch(token, info)) {
                String msg = "";
                throw new IncorrectCredentialsException(msg);
            }
        } else {
            throw new AuthenticationException("");
        }
    }

 CredentialsMatcher的实现类这里是SimpleCredentialsMatcher它的doCredentialsMatch方法实现如下:

//这个方法很简单就是将token中的密码跟后台获取用户信息中的密码进行比对,返回比对结果
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenCredentials = getCredentials(token);
        Object accountCredentials = getCredentials(info);
        return equals(tokenCredentials, accountCredentials);
    }

 到此shiro认证过程结束,当然这个是在前面HelloWorld例子中的流程,不过一般web项目的实现内部也大差不差

相关推荐