08、Shiro 实战:Shiro密码的比对
上一次总结了如何实现一个简单的Shiro认证流程。首先通过前端页面的form表单提交,在Controller请求处理层获取了form表单中的账号密码,然后获取当前用户的Subject对象,执行了Subject的login方法进行登录操作,并将账号密码封装进Token对象,作为参数传入。而后面设置了认证需要的Realm类,该Realm类继承了AuthenticatingRealm父类,实现了doGetAuthenticationInfo方法,在doGetAuthenticationInfo方法中获取用户的账号密码,在做完一些校验后,传递给了SimpleAuthenticationInfo,并返回出去:
package com.test.shiro.realms;
import java.util.HashMap;
import java.util.Map;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.AuthenticatingRealm;
import com.test.shiro.po.User;
public class ShiroRealm extends AuthenticatingRealm{
private static Map<String,User> userMap = new HashMap<String,User>();
static{
//使用Map模拟数据库获取User表信息
userMap.put("jack", new User("jack","aaa123",false));
userMap.put("tom", new User("tom","bbb321",false));
userMap.put("jean", new User("jean","ccc213",true));
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//2.从UsernamePasswordToken中获取username
String username = userToken.getUsername();
//3.调用数据库的方法,从数据库中查询Username对应的用户记录
System.out.println("从数据看中获取UserName为"+username+"所对应的信息。");
//Map模拟数据库取数据
User u = userMap.get(username);
//4.若用户不行存在,可以抛出UnknownAccountException
if(u==null){
throw new UnknownAccountException("用户不存在");
}
//5.若用户被锁定,可以抛出LockedAccountException
if(u.isLocked()){
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构建AuthenticationInfo对象,通常使用的实现类为SimpleAuthenticationInfo
//以下信息是从数据库中获取的
//1)principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
Object principal = u.getUsername();
//2)credentials:密码
Object credentials = u.getPassword();
//3)realmName:当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,realmName);
return info;
}
}
可以看到,代码中并没有账号密码的校验,那么Subject对象是如何通过Realm返回的AuthenticationInfo对象来进行账号密码比对的呢?
通过上面的登录流程我们可以知道,目前有两个对象是保存有用户的账号和密码信息的,一个是封装了前台用户在form表单中填写的账号密码信息的UsernamePasswordToken,一个是封装了从数据库取出的对应账号的账号密码信息的SimpleAuthenticationInfo对象。我们可以推测出,Shiro的密码比对一定是通过这两个对象进行比较的,那么Shiro是什么时候进行密码比对的呢?
其实并不难知道Shiro的校验位置,因为在Shiro进行密码比对时,一定会去拿UsernamePasswordToken和SimpleAuthenticationInfo中封装的密码信息,那么此时要调用UsernamePasswordToken的getPassword方法,或者调用SimpleAuthenticationInfo的getCredentials方法。
既然这样,可以关联UsernamePasswordToken的源代码,然后找到其getPassword方法,在该处打断点:
这样我们重新运行校验实例程序的时候,就可以发现校验的时机了。
我们在前台输入账号密码点击登录:
然后发现断点停止了,查看Debug的视窗,看一下在获取密码之前的操作是什么:
看到前四步的SimpleCredentialsMatcher有一个doCredentialsMatch方法,在该方法中就进行了密码比对工作:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}
还可以看到,是我们自定义的ShiroRealm(AuthenticatingRealm为父类)执行的assertCredentialsMatch方法调用的该doCredentialsMatch方法进行密码比对:
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(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.");
}
}
总结一下也就是,密码的具体比对工作是我们自定义的继承了AuthenticatingRealm父类的自定义Realm类调用CredentialsMatcher的doCredentialsMatch方法完成的。
目前,我们的密码比对是直接明文对比的,在实际的生产环境中,密码往往会进行加密工作,那么我们如何进行密码的加密呢?下一篇将介绍如何使用MD5盐值加密。