在上一篇博文 springboot整合shiro入门 中,简单介绍了如何使用shiro进行认证和授权,下面通过debug的方式(示例代码还是上一篇博客使用的代码),分析一下shiro是如何进行认证的:
首先,回顾一下,处理登录的方法:
@PostMapping("/doLogin")
@ResponseBody
public String doLogin(String username,String password){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return "登录成功";
}catch (UnknownAccountException|IncorrectCredentialsException e){
e.printStackTrace();
return "账号或密码错误";
}catch (LockedAccountException e){
e.printStackTrace();
return "账号已被锁定,请联系管理员";
}catch (AuthenticationException e){
e.printStackTrace();
return "未知异常,请联系管理员";
}
}
可以发现,登录过程,调用的方法是:
subject.login(token);
这里要debug的话,有两种方式:
(1)通过一步步debug跟进去看看它做了什么,这种方法比较通用,但是如果跟得很深的话,很容易晕。。。
(2)另外一种就是比较取巧的方法,因为认证,肯定是要拿到用户名和密码进行比较的,而我们的用户名和密码都封装到了 UsernamePasswordToken这个类中,那么我们完全可以把断点打在UsernamePasswordToken的获取用户名和密码的方法上,然后看看方法调用栈即可知道其调用了哪些类的哪些方法了:
现在就可以启动项目,访问登录接口进行登录认证了,然后发现很顺利的进入了获取用户名的断点了:
那么,我们从doLogin的subject.login(token)方法,一路看到UsernamePasswordToken获取用户名的方法,看看它做了什么:
1. 首先是调用主体subject的login方法,把UsernamePasswordToken作为参数传入进去了:
subject.login(token);
2.进入到 DefaultSecurityManager 的login方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
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);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
并且停在了:
info = authenticate(token);
其中,info就是要返回的认证信息,看一下DefaultSecurityManager的继承关系,会发现它继承了SessionsSecurityManager,而SessionsSecurityManager继承了AuthenticatingSecurityManager,所以接下来它调用的是父类的认证方法
3.调用父类AuthenticatingSecurityManager的authenticate()方法:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
发现它调用的是authenticator的authenticate()方法,那么authenticator是什么呢?它其实是ModularRealmAuthenticator,在构造方法里面进行了初始化:
4. 调用ModularRealmAuthenticator的doAuthenticate()方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
//获取所有的realm
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
它会先获取所有的realm,然后判断,如果只有个一个realm的话,就执行doSingleRealmAuthentication()方法,否则的话就执行doMultiRealmAuthentication()方法,这里因为只有一个realm,所以执行的doSingleRealmAuthentication
5.调用ModularRealmAuthenticator的doSingleRealmAuthentication()方法:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
可以发现,它会调用:
AuthenticationInfo info = realm.getAuthenticationInfo(token);
这里的realm,就是我们自定义的realm,而realm.getAuthenticationInfo(token);会先从缓存中取用户信息,如果没有,就会调用doGetAuthenticationInfo()方法:
info = doGetAuthenticationInfo(token);
而这个doGetAuthenticationInfo()就是我们自定义realm时重写的方法。
6.执行自定义realm的doGetAuthenticationInfo方法,获取用户信息:
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
//这里模拟数据库查询用户,根据用户名查询
User dbUser = userService.getUserByUsername(username);
if (dbUser == null){
//账号不存在
throw new UnknownAccountException();
}
if (dbUser.getEnable()==0){
//账号被锁定
throw new LockedAccountException();
}
return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), getName());
}
到了这里,就会根据前端传入的用户名到数据库查询用户信息,封装成SimpleAuthenticationInfo返回。
其实,现在只是验证了该用户是否存在,以及账号是否被锁定等,并没有通过验证,因为密码都还没有比对。
所以放开获取用户名的断点,看看接下来的密码是如何比对的,现在到了获取密码的这个断点:
回到AuthenticatingRealm的getAuthenticationInfo()方法,
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从缓存中获取用户信息
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//如果缓存中没有,则调用自定义realm的doGetAuthenticationInfo获取
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;
}
可以看到,有这么一行代码:
//断言密码是匹配的
assertCredentialsMatch(token, info);
那么,它肯定会在这里进行密码比对的,判断密码是否正确,我们跟下去看一下assertCredentialsMatch()方法:
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.");
}
}
其中,cm是SimpleCredentialsMatcher类的实例,所以会调用它的doCredentialsMatch进行密码匹配:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//获取前端传入的密码,也就是用户输入的密码
Object tokenCredentials = getCredentials(token);
//获取数据库或者内存中存储的密码
Object accountCredentials = getCredentials(info);
//比对两个密码,判断是否一致
return equals(tokenCredentials, accountCredentials);
}
这里思路也很清晰,就是获取用户输入的密码和存储的密码,然后比较两个密码是否相同。如果密码相同,那么身份认证就成功完成了。否则就会抛出异常。
到这里,就完成了身份认证。
简单总结一下认证流程:
1、调用Subject.login()方法
2、委托给DefaultSecurityManager的login方法
3、DefaultSecurityManager进一步委托给ModularRealmAuthenticator,调用其doAuthenticate()方法:
4、ModularRealmAuthenticator则会获取到所有的realm,来获取用户信息
5、调用具体的realm的getAuthenticationInfo获取用户信息,缓存有则返回,否则通过doGetAuthenticationInfo来获取用户信息
6、获取完用户信息之后,则会调用SimpleCredentialsMatcher的doCredentialsMatch()方法进行密码匹配,如果密码相同,那么身份认证就成功完成了,否则就会抛出异常。