用户的信息多都在Realm中,所以我们先从realm开始看源码吧。
我们在上一篇中用到了AuthenticationRealm,这个就是Realm接口的实现类,我们具体看这个类的源码以及在校验时所用到的方法。
先从这个类的无参构造方法开始
public AuthenticatingRealm() {
this(null, new SimpleCredentialsMatcher());
}
其中第一个参数是CacheManager,是一个null,第二个是CredentialsMatcher。
AuthenticationRealm是CachingRealm的实现类,CachingRealm允许对将用户已经获得AuthenticationIfon进行缓存,方便下一次使用。但是这个缓存默认是关闭的,
AuthenticationRealm构造方法中的null表示没有传入CaqcheManager,我们在实际开发中都是关闭缓存,改用redis来实现对用户信息的缓存。
第二个参数CredentialsMatcher很重要,他用来将用户通过表单提交的密码和在数据库中的密码进行比对,判断是不是一直,默认提供的实现类是SimpleCredentialsMatcher,这个类的对比方式如下:
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" +
tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
accountCredentials.getClass().getName() + "]");
}
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " +
"array equals comparison");
}
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return Arrays.equals(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
我们一般采用字符串作为密码,假设这样的话,会将字符串转化为数组,采用的办法是
String.getBytes("UTF-8")
然后调用Arrays.equals的方法。从这里我们看出,
SimpleCredentialsMatcher没有涉及任何的加密算法,只适合于我们的普通测试字符串是否相同,如果是加密的我们需要自己定义自己的加密算法和使用加密算法进行对比的CredentialMatcher,但是在实际中我们更倾向于这样做:
在数据库中存储的是经过加密的密码,将用户提交的密码在进行对比之前我们自己使用相同的办法将其进行加密,然后仍然使用SimpleCredentialsMatcher,这样就避免了创建CredentialMatcher的麻烦了。在接触shiro前我的加密算法都是采用的md5 + base64,幸运的是shiro也提供了这两种加密算法,我推荐搭建使用shiro自带的加密类,然后将其封装成一个工具类,我的代码如下:
import org.apache.shiro.crypto.hash.Md5Hash;
public class EncryptUtil {
public String toMd5(String pwd){
return new Md5Hash(pwd,"salt2016",111).toBase64();
}
}
shiro提供了Hash接口,表示对明文加密,其中Md5Hash只是个例(这个不是重点,也可以使用Md2Hash)
具体解释如下:org.apache.shiro.crypto.hash.Md5Hash.Md5Hash(Object source, Object salt, int encryptionTime) 第一个参数表示要加密的密码,第二个表示加密过程中加的盐,第三个表示加密的次数,也就是明文加密一次后再对新生成的暗纹继续加密,还有很多重载的Md5Hash构造方法,并且可以不用加盐,也不用指定加密的次数。最后toBase64是用来格式化显示的,因为调用md5加密算法之后会有一些不友好的字符,通过base64将其完全转化为键盘上的字符,这样更加友好。(也可以toHex,只是算法不同,意思一样)
继续我们的AuthenticatingRealm 在另一个构造方法中 AuthenticatingRealm(CacheManager, CredentialsMatcher),我们看一下源码
public AuthenticatingRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
authenticationTokenClass = UsernamePasswordToken.class;
//retain backwards compatibility for Shiro 1.1 and earlier. Setting to true by default will probably cause
//unexpected results for existing applications:
this.authenticationCachingEnabled = false;
int instanceNumber = INSTANCE_COUNT.getAndIncrement();
this.authenticationCacheName = getClass().getName() + DEFAULT_AUTHORIZATION_CACHE_SUFFIX;
if (instanceNumber > 0) {
this.authenticationCacheName = this.authenticationCacheName + "." + instanceNumber;
}
if (cacheManager != null) {
setCacheManager(cacheManager);
}
if (matcher != null) {
setCredentialsMatcher(matcher);
}
}
其中指定了默认支持的AuthenticationTokenClass是UsernamePassowrdToken,所以我们在上一节的代码中使用了UsernamePasswordToken,如果不适用这个的话必须改对AuthenticationReal进行设置,否则会不支持的。从这个代码中发现默认是不支持缓存的,所以关于缓存的我们就可以放心了。然后在这个构造方法中设置了CredentialsMather,就是上面介绍的SimpleCredentialsMatcher。
当某个用户登录时,通过dubug发现调用的是AuthenticatingRealm的getAuthenticationInfo方法,就是通过传入的UsernamePasswordToken,将源码拿出来
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
上面代码的 大致意思是先从以前缓存的获取,因为我们不做缓存,所以忽略这个,如果缓存中没有的话调用的是doGetAuthenticationInfo(token)方法,这个就是我们上一节中覆写的方法,用来从数据库中获得用户的信息。最后assertCredentialsMatcher调用之前设置的CredentialMatcher进行对比传递进来的token和最后从数据库中获取的info是否一致,我们看一下assertCredentialsMatcher方法的源码:
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."); } }如果对比一致的话,没有任何返回或者异常,但是如果对比不一致就会抛IncorrectCredentialsException。这里很容易理解,问题是我们知道AuthenticationException有很多子类,如果是其他异常呢,应该在哪里抛,但是是在我们复写的doGetAuthenticationInfo方法中,如果有其他异常,可以直接在这里抛,比如上一节我们复写的方法中就抛出了UnknownAccountException,我们可以根据我们的业务逻辑规定自己的异常,但是别忘了定义的异常要继承AuthenticationException。 接下来是AuthenticationInfo,表示根据用户名从数据库中获得的用户的信息,在我自己的实现中是直接new了一个匿名类,其实shiro是给了我们实现类的——SimpleAuthenticationInfo,我们可以直接实例化这个类,改写的代码如下:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { final String username = (String) token.getPrincipal(); final String password = db.get(username); if(db.get(username)==null){ throw new UnknownAccountException(); } AuthenticationInfo ainfo = new SimpleAuthenticationInfo(username, password, "DB"); return ainfo; }我们进入SimpleAuthenticationInfo的构造方法中,代码如下:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) { this.principals = new SimplePrincipalCollection(principal, realmName); this.credentials = credentials; }里面是实例化了一个PrincipalCollection的实现类,进入到里面,代码如下:
public SimplePrincipalCollection(Object principal, String realmName) { if (principal instanceof Collection) { addAll((Collection) principal, realmName); } else { add(principal, realmName); } }由于我们从一开始就假设我们只有一个realm所以我们传入的pricipal是一个String类型的对象,所以上面的代码执行的是add方法,add方法最终执行的是这个代码getPrincipalsLazy(realmName).add(principal);其中getPrincipalLazy的代码如下:
protected Collection getPrincipalsLazy(String realmName) {
if (realmPrincipals == null) {
realmPrincipals = new LinkedHashMap<String, Set>();
}
Set principals = realmPrincipals.get(realmName);
if (principals == null) {
principals = new LinkedHashSet();
realmPrincipals.put(realmName, principals);
}
return principals;
}
其中realmPrincipals是SimplePrincipalCollection的一个属性,表示按照realm按照pricipal进行分开,存在一个hashmap中,key是realm的名字,value就是一个set,这里它使用了LinkedHashMap,是为了保证顺序,方便下面的getPrimaryPrincipal使用,由于我们只有一个Realm,所以不用特别关心这些。 通过上面的分析我们可以发现,
SimpleAuthenticationInfo(username, password, "DB");这行代码是将username作为一个pricipal存到HashMap中key为DB的set中了。 还有一个比较重要的类PrincipalCollection,我们在复写的doGetAuthenticationInfo方法中有这个类,在刚才的分析中也发现了他的实现类SimplePrincipalCollection,它里面有一个很重要的方法:getPrimaryPrincipal,表示获得所有的pricipal中最主要的一个,我们看一下SimplePrincipalCollection的源码:
public Object getPrimaryPrincipal() {
if (isEmpty()) {
return null;
}
return iterator().next();
}
然后进入他的iterator方法发现调用的是如下的方法asSet
public Set asSet() { if (realmPrincipals == null || realmPrincipals.isEmpty()) { return Collections.EMPTY_SET; } Set aggregated = new LinkedHashSet(); Collection<Set> values = realmPrincipals.values(); for (Set set : values) { aggregated.addAll(set); } if (aggregated.isEmpty()) { return Collections.EMPTY_SET; } return Collections.unmodifiableSet(aggregated); }如果是多个realm的话会将realmPrincipals(就是上面提到的那个根据realm区分pricipal的map)中的所有pricipal都放到一个set去,这里我们联想之前提到的那个LinkedHashMap,就能明白作者的用意——作者是想保存顺序,因为在getPrimaryPrincipal方法中直接就是将返回的iteration调用的next方法,即取得第一个值。所以如果是多个域的话,第一个放入的principal是primaryPrincipal。当然我们只有一个域,所以得到的primaryPrincipal就是我们那个唯一的域了。 学到这里,用户登录基本就可以做完了,下一节将介绍访问权限的控制。