【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.将前后台获取的用户信息进行比对,验证是否通过
回顾一下之前的架构:
注意其中的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项目的实现内部也大差不差