问题:Shrio是如何通过认证的
-
背景:使用
DefultWebSecurityManage
实现用户登录认证,并且自定义一个Realm -
线索:
(1)在配置文件中,ShiroFilterFactoryBean
依赖与SecurityManager
,SecurityManager
依赖于Realm
,也就是说在代码中这几个之间构成了某种关系
(2)先弄清楚Realm
的实现类,Subject
的实现类更加容易理解方法的调用,他们贯穿了整个认证流程 -
基于Shiro实现的登录逻辑
1.用户输入login.do并且加入user和password,回车 2.用户接受参数,并组装为Token,从SecurityUtils中获取Subject 3.调用login方法,开始认证 4.重写的Realm类中,调用doGetAuthenticationInfo(AuthenticationToken token) 5.从token中拿到user,查询数据库,得到User对象 6.并将User对象的信息封装到AuthenticationInfo中,返回Info给SecurityManager 7.manger将拿到的ifo和token进行对比,完成认证
分析源码的实现
//开始login
subject.login(token);
1.DelegatingSubject是这个subject的实现类,里面的Login方法
public class DelegatingSubject implements Subject {
....
public void login(AuthenticationToken token) throws AuthenticationException {
//这个方法是为例清楚session里面的信息
clearRunAsIdentitiesInternal();
/**这里是认证的重点*/
/**这里是认证的重点*/
/**这里是认证的重点*/
Subject subject = securityManager.login(this, token);
/**看上面,主要功能是认证,其他代码省略,可能下一篇会把整个逻辑的代码注解一遍*/
}
....
}
2.上面的securityManager.login(this,token),本质上是DefultSecurityManager.login()
并且反回一个DelegatingSubject对象
public class DefaultSecurityManager extends SessionsSecurityManager {
...
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
//设置接受AuthenticationInfo
AuthenticationInfo info;
/**处理异常的代码省略*/
...
//最主要的方法
//最主要的方法
//最主要的方法
info = authenticate(token);
//看上面
...
//创建subject对象,里面包含了认证之后几乎所有的信息
Subject loggedIn = createSubject(token, info, subject);
//读取token中的Rememberme信息
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
...
}
3.这个authenticate(token)
,实际上是AuthenticatingSecurityManager
类里面的公有方法,并且DefultSecurityManager
的父类的父类的父类是这个类,所以可以直接调用这个方法。
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
/**这个Authenticator里面的是ModularRealmAuthenticator,这个类继承了实现Authenticator接口的AbstractAuthenticator类*/
private Authenticator authenticator;
public AuthenticatingSecurityManager() {
super();
this.authenticator = new ModularRealmAuthenticator();
}
...
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
...
}
4.所以这个authenticate(token),实际上是
new ModularRealmAuthenticator().authenticate(token);
注意这里面的authenticate()是在AbstractAuthenticator类中被final修饰,所以不能被重写。
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
...
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
...
//核心部分
//核心部分
//看函数名是要开始做授权
info = doAuthenticate(token);
//看上面
...
}
...
}
6.还是跳到ModularRealmAuthenticator里面的doAuthenticate(token)
public class ModularRealmAuthenticator extends AbstractAuthenticator {
...
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
//获取所有的realm
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
//因为我们前面我们只定义了一个Realm,并且通过setRealm(realm),所以调用这个方法
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
//当我们定义多个Realm时到这里
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
...
}
7.选择适合当前Realm的函数经行认证,也就是doSingleRealmAuthenticatication()
,这个方法也是ModularRealmAuthenticator
类下面的方法
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
//用来判断该Realm是否支持这个token,这里时在定义Realm时决定的
....
}
//核心代码
//核心代码
//核心代码
AuthenticationInfo info = realm.getAuthenticationInfo(token);
//看上面
if (info == null) {
....
//这里面时在验证token时未找到相应的账户信息
}
return info;
}
8.在之前也说了,这个realm是我们最开始配置的CoustomRealm
类,所以这个调用的getAuthentiactionInfo(token)
其实也就是我们自己定义的CoustomRealm
所继承自AuthenticatingRealm
类的方法
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
....
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从认证缓存中拿到token的相关信息,这是第一次认证,故没有缓存数据
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
/**
核心代码一
主要是通过我们自定义的Realm获得对应的token数据,所以才需要判断是否支持这个token
如:token对应的数据库中的账号、密码等
*/
info = doGetAuthenticationInfo(token);
//看上面
....
if (token != null && info != null) {
//如果成功,缓存身份验证信息
cacheAuthenticationInfoIfPossible(token, info);
}
}
....
if (info != null) {
/**
核心代码二
判断凭证是否匹配,如果不匹配会抛出异常,匹配成功无返回值
*/
assertCredentialsMatch(token, info);
}
.....
return info;
}
....
}
10.前面已经强调了Realm是我们自己定义的CoustomRealm,所以doGetAuthenticationInfo(token)
本质上其实是new CoustomRealm().doGetAuthenticationInfo(token)
。(下面是我自己自己定义的Realm,可以参考Realm的工作流程)
public class CustomRealm extends AuthorizingRealm {
....
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//权限的认证省略,跟认证查询信息相似
....
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (username == null)
throw new UnknownAccountException("用户名不能为空");
UserExample example = new UserExample();
example.or().andUsernameEqualTo(username);
List<User> list = userRepository.selectByExample(example);
if (list.isEmpty())
throw new UnknownAccountException("不存在用户名为[" + username + "]的用户");
if (list.size() != 1)
throw new RuntimeException("数据库错误。存在多位用户名为[" + username + "]的用户");
User user = list.get(0);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, user.getPassword(), CustomRealm.class.getName());
info.setCredentialsSalt(ByteSource.Util.bytes(user.getPasswordSalt()));
return info;
}
}
11.拿到Info信息,里面是包含通过token获取的用户名查到的账号,密码等信息,接下来回到上面的第8步的核心代码二中:assertCredentialsMatch(token, info);
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
....
//如果匹配失败,就抛出一个IncorrectCredentialsException异常
}
//成功的话结束了这个函数的运行,并且无返回值
} else {
//如果没有CredentialsMatcher 就抛出一个AuthenticationException异常
}
}
12.认证到此结束,当然认证成功之后会通过sessionDao添加信息到相应的session里面。