在上一篇博文 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()方法進行密碼匹配,如果密碼相同,那麼身份認證就成功完成了,否則就會拋出異常。