一、shiro認證流程原始碼
使用shiro框架做登錄,只需呼叫subject的login方法即可,代碼如下:
public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
{
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try
{
subject.login(token);
return success();
}
catch (AuthenticationException e)
{
String msg = "用戶或密碼錯誤";
if (StringUtils.isNotEmpty(e.getMessage()))
{
msg = e.getMessage();
}
return error(msg);
}
}
下面,就看subject的login方法內部實作思路,
通過debug,進入了DelegatingSubject類的login方法:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
可以看出,里面呼叫了securityManager的login方法,繼續進入該方法,最終,走到了AbstractAuthenticator類的authenticate方法,這就是Shiro的認證方法,最終追到了如下方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
這里,獲取realm域,如果是一個,那就是單資料庫的,如果是多個,那就是多資料庫的,需要從多個資料庫中查詢用戶資訊,這里我們看單realm的方法doSingleRealmAuthentication,重點代碼來到如下方法:
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方法就呼叫了我們自定義的Realm類的doGetAuthenticationInfo方法,從資料庫查詢用戶資訊,然后回傳,

自定義Realm類的doGetAuthenticationInfo方法如下:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
{
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
String password = "";
if (upToken.getPassword() != null)
{
password = new String(upToken.getPassword());
}
SysUser user = null;
try
{
user = loginService.login(username, password);
}
catch (CaptchaException e)
{
throw new AuthenticationException(e.getMessage(), e);
}
catch (UserNotExistsException e)
{
throw new UnknownAccountException(e.getMessage(), e);
}
catch (UserPasswordNotMatchException e)
{
throw new IncorrectCredentialsException(e.getMessage(), e);
}
catch (UserPasswordRetryLimitExceedException e)
{
throw new ExcessiveAttemptsException(e.getMessage(), e);
}
catch (UserBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
catch (RoleBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
catch (Exception e)
{
log.info("對用戶[" + username + "]進行登錄驗證..驗證未通過{}", e.getMessage());
throw new AuthenticationException(e.getMessage(), e);
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
可見,其就是通過查詢資料庫,獲取用戶資訊,然后和傳入的token做對比,判斷認證是否通過,
至此,登錄驗證程序完成,通過上面的分析我們可以知道,shiro給程式員留出的口就是查詢資料庫,對比token,判斷是否登錄成功,這似乎和我們不使用shiro框架的操作一樣啊,不使用shiro框架不也是查詢資料庫,比較用戶名密碼,判斷是否登錄嗎?那使用shiro還有啥用呢?我們看認證成功后,shiro又做了哪些操作,
二、認證成功后續操作流程原始碼
接著上面getAuthenticationInfo方法看,
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;
}
可以看到,認證成功后,會呼叫cacheAuthenticationInfoIfPossible方法,進行快取,看其原始碼,
private void cacheAuthenticationInfoIfPossible(AuthenticationToken token, AuthenticationInfo info) {
if (!isAuthenticationCachingEnabled(token, info)) {
log.debug("AuthenticationInfo caching is disabled for info [{}]. Submitted token: [{}].", info, token);
//return quietly, caching is disabled for this token/info pair:
return;
}
//獲取到Cache物件
Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
if (cache != null) {
//通過token,生成一個key,
Object key = getAuthenticationCacheKey(token);
//將key和登錄資訊info存入快取中,
cache.put(key, info);
log.trace("Cached AuthenticationInfo for continued authentication. key=[{}], value=[{}].", key, info);
}
}
這里可以知道,認證成功后,shiro將認證資訊存入了快取物件Cache中,
我們繼續往上回查代碼,看認證成功得到info后,還有哪些操作,
在AbstractAuthenticator的authenticate認證方法中,認證成功后,呼叫了如下方法:
notifySuccess(token, info);
看其方法內部:
protected void notifySuccess(AuthenticationToken token, AuthenticationInfo info) {
for (AuthenticationListener listener : this.listeners) {
listener.onSuccess(token, info);
}
}
可以看出,這個方法利用了觀察者模式,用于認證成功后,通知AuthenticationListener 的實作類,這里提供了監聽用戶認證成功的豁口,所以,我們想在一個用戶登錄時做一些操作的話,可以實作AuthenticationListener 介面來做操作,如提醒誰上線的需求,就可以用這個豁口來實作,AuthenticationListener 類的研究我們單獨講解,這里看流程,
繼續往上回查代碼,看SecurityManager的login方法,看這行代碼:
Subject loggedIn = createSubject(token, info, subject);
這里,根據info認證資訊,生成了subject物件,點進去看方法,
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);//設定SecurityManager
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);//設定session
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);//設定用戶登錄資訊
Subject subject = doCreateSubject(context);//生成subject
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
可以看出,創建subject,就是往subject里設定了SecurityManager,session和Principals資訊,
然后,看SecurityManager的login方法的這行代碼;
onSuccessfulLogin(token, info, loggedIn);
這行代碼是"記住我"功能的支持,這里我們分析主流程,這行代碼我們單獨分析,繼續往回追代碼,到了DelegatingSubject的login方法,看剩余的方法:
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
可以看出,就是對session,principals等一些資料的初始化賦值操作,至此,subject的login方法流程分析完成,
三、總結
下面,總結一下上面的分析程序,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/400457.html
標籤:其他
