【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项目的实现内部也大差不差