1. 程式人生 > >shiro原始碼分析篇3:使用者登入快取登入資訊

shiro原始碼分析篇3:使用者登入快取登入資訊

上篇講了shiro是如何過濾請求連結,判斷使用者是否已經登入。

這篇就是講解shiro使用者登入時,如何把登入資訊快取起來,下次使用者登入其他需要登入的連結時,如何判斷已經登入了。

RetryLimitHashedCredentialsMatcher自定義的登入憑據,也就是登入的處理方案

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

    private Logger logger = LoggerFactory.getLogger(RetryLimitHashedCredentialsMatcher.class);
    private
Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1
AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw
logger.info("===========嘗試超過5次=============="); throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; } }

UserRealm:Realm:域,Shiro 從從 Realm 獲取安全資料(如使用者、角色、許可權),就是說 SecurityManager 要驗證使用者身份,那麼它需要從 Realm 獲取相應的使用者進行比較以確定使用者身份是否合法;

UserController:使用者登入功能

@Controller
public class UserController {
   @RequestMapping("DoLogin")
    public ModelAndView login(String username,String password){

        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        Subject subject = SecurityUtils.getSubject();
        if(subject.isAuthenticated()){
            return new ModelAndView("admin");
        }
        try {
            subject.login(token);
            if(subject.isAuthenticated()){
                return new ModelAndView("admin");
            }
        }catch (Exception e){

        }
        return new ModelAndView("login");
    }
}

原始碼請點選:https://github.com/smallleaf/cacheWeb

瀏覽器輸入http://localhost:8080/DoLogin?username=admin&password=admin

斷點打到DelegatingFilterProxy.doFilter。和UserController.login
我們走起
請求肯定是會先經過DelegatingFilterProxy進行shiro過濾的過濾功能的。和上篇一樣。
這裡我們分析,為什麼沒有登入的時候。subject.getPrincipal() ==null。

我們debug進入AbstractShiroFIlter.doFilterInternal。
final Subject subject = createSubject(request, response);
最後進入

 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);

        //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);

        //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);

        //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;
    }

這裡上篇已經分析了。我們要分析的地方就是context = resolvePrincipals(context);為什麼subject.getPrincipal() ==null。
進入

 public PrincipalCollection resolvePrincipals() {
        PrincipalCollection principals = getPrincipals();

        if (CollectionUtils.isEmpty(principals)) {
            //check to see if they were just authenticated:
            AuthenticationInfo info = getAuthenticationInfo();
            if (info != null) {
                principals = info.getPrincipals();
            }
        }

        if (CollectionUtils.isEmpty(principals)) {
            Subject subject = getSubject();
            if (subject != null) {
                principals = subject.getPrincipals();
            }
        }

        if (CollectionUtils.isEmpty(principals)) {
            //try the session:
            Session session = resolveSession();
            if (session != null) {
                principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
            }
        }

        return principals;
    }

PrincipalCollection principals = getPrincipals();沒有登入這裡一定null。
AuthenticationInfo info = getAuthenticationInfo();同樣也null。
Subject subject = getSubject();同樣為null。未建立嘛。
Session session = resolveSession();如果這裡不是第一次請求這個網站那麼此時session不為null。為什麼我上篇已經分析了。這裡再提一下,第一次伺服器返回一個session給客戶端,並且快取到伺服器,以後每次客戶端都是拿這個sessionId去請求。
session不為null。那麼
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);我們從session中取出PRINCIPALS_SESSION_KEY。
如果PRINCIPALS_SESSION_KEY該session中存在改值,是不是principals不為null。那麼是不是意味著已經登入。

於是接下來,我們的任務就是找到哪裡,儲存這個principals到session中,並且快取下來,這樣我們的目的就已經達到了。

未登入當然是找不到的。
登入請求連結是不會被shiro所攔截的,因此執行完這個基本的過濾器,之後就返回到我們登入的介面了。

subject.login(token);

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;
        }
    }

主要看:Subject subject = securityManager.login(this, token);

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);就是進行登入資訊的驗證。
Subject loggedIn = createSubject(token, info, subject);登入成功後,建立subject,那麼就是這裡進行session儲存的。

先看如何進行身份驗證。

    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只有UserRealm。所以執行: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;
    }

這裡回頭看看UserRealm。看看它的繼承鏈

public class UserRealm extends AuthorizingRealm
public abstract class AuthorizingRealm extends AuthenticatingRealm
        implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware 

AuthenticationInfo info = realm.getAuthenticationInfo(token);
其實就是父類AuthenticatingRealm.getAuthenticationInfo

private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
        AuthenticationInfo info = null;

        Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
        if (cache != null && token != null) {
            log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
            Object key = getAuthenticationCacheKey(token);
            info = cache.get(key);
            if (info == null) {
                log.trace("No AuthorizationInfo found in cache for key [{}]", key);
            } else {
                log.trace("Found cached AuthorizationInfo for key [{}]", key);
            }
        }

        return info;
    }

先從快取中獲取這Realm資訊,如果沒有就執行.
info = doGetAuthenticationInfo(token);這裡doGetAuthenticationInfo是一個抽象方法也就是UserRealm實現的。取到這個認證資訊後。進行資訊比較
assertCredentialsMatch(token, info);

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.");
        }
    }

首先獲得憑證匹配器。也就是我們自定義的RetryLimitHashedCredentialsMatcher。

  public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String username = (String)token.getPrincipal();
        //retry count + 1
        AtomicInteger retryCount = passwordRetryCache.get(username);
        if(retryCount == null) {
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(username, retryCount);
        }
        if(retryCount.incrementAndGet() > 5) {
            //if retry count > 5 throw
            logger.info("===========嘗試超過5次==============");
            throw new ExcessiveAttemptsException();
        }

        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            //clear retry count
            passwordRetryCache.remove(username);
        }
        return matches;
    }

其實就是在進行比較時,做了一些個性化操作。

   public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }

這裡我就不分析了,無非就是進行認證資訊加密啥的,然後和伺服器也就是Realm返回的資訊比對,看看是不是登入成功了。

這裡我們假設使用者名稱密碼正確返回ture。

認證成功就到了Subject loggedIn = createSubject(token, 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);

        //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);

        //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);

        //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;
    }

又回到了這裡。

public Subject createSubject(SubjectContext context) {
        if (!(context instanceof WebSubjectContext)) {
            return super.createSubject(context);
        }
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals();
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();

        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
                request, response, securityManager);
    }

createSubject就是初始化一些資訊,到WebDelegatingSubject然後返回。

重點到 save(subject);這裡就是我們要快取session的地方了。

 protected void save(Subject subject) {
        this.subjectDAO.save(subject);
    }

  public Subject save(Subject subject) {
        if (isSessionStorageEnabled(subject)) {
            saveToSession(subject);
        } else {
            log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
                    "authentication state are expected to be initialized on every request or invocation.", subject);
        }

        return subject;
    }

   protected void saveToSession(Subject subject) {
        //performs merge logic, only updating the Subject's session if it does not match the current state:
        mergePrincipals(subject);
        mergeAuthenticationState(subject);
    }

mergePrincipals(subject);也就是儲存principals的地方。

 protected void mergePrincipals(Subject subject) {
        //merge PrincipalCollection state:

        PrincipalCollection currentPrincipals = null;

        //SHIRO-380: added if/else block - need to retain original (source) principals
        //This technique (reflection) is only temporary - a proper long term solution needs to be found,
        //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
        //
        //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
        if (subject.isRunAs() && subject instanceof DelegatingSubject) {
            try {
                Field field = DelegatingSubject.class.getDeclaredField("principals");
                field.setAccessible(true);
                currentPrincipals = (PrincipalCollection)field.get(subject);
            } catch (Exception e) {
                throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
            }
        }
        if (currentPrincipals == null || currentPrincipals.isEmpty()) {
            currentPrincipals = subject.getPrincipals();
        }

        Session session = subject.getSession(false);

        if (session == null) {
            if (!CollectionUtils.isEmpty(currentPrincipals)) {
                session = subject.getSession();
                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
            }
            //otherwise no session and no principals - nothing to save
        } else {
            PrincipalCollection existingPrincipals =
                    (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);

            if (CollectionUtils.isEmpty(currentPrincipals)) {
                if (!CollectionUtils.isEmpty(existingPrincipals)) {
                    session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
                }
                //otherwise both are null or empty - no need to update the session
            } else {
                if (!currentPrincipals.equals(existingPrincipals)) {
                    session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
                }
                //otherwise they're the same - no need to update the session
            }
        }
    }

currentPrincipals = subject.getPrincipals();返回當前的principals。
Session session = subject.getSession(false);不建立session。直接獲取。
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
這裡是不是很眼熟。我們回到上面過濾是取principals的地方。

 if (CollectionUtils.isEmpty(principals)) {
            //try the session:
            Session session = resolveSession();
            if (session != null) {
                principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
            }
        }

這裡就把認證成功的資訊儲存到了session當中,我們只需要知道快取的地方就可以。
我們這裡提到的session已經不是sevlert中原生態的session了。已經是被shiro給封裝過了的。

session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException {
        if (value == null) {
            removeAttribute(attributeKey);
        } else {
            sessionManager.setAttribute(this.key, attributeKey, value);
        }
    }

public void setAttribute(SessionKey sessionKey, Object attributeKey, Object value) throws InvalidSessionException {
        if (value == null) {
            removeAttribute(sessionKey, attributeKey);
        } else {
            Session s = lookupRequiredSession(sessionKey);
            s.setAttribute(attributeKey, value);
            onChange(s);
        }
    }
  protected void onChange(Session session) {
        sessionDAO.update(session);
    }
 public void update(Session session) throws UnknownSessionException {
        doUpdate(session);
        if (session instanceof ValidatingSession) {
            if (((ValidatingSession) session).isValid()) {
                cache(session, session.getId());
            } else {
                uncache(session);
            }
        } else {
            cache(session, session.getId());
        }
    }

這裡我們已經發現,快取更新了session。

那麼當我們訪問http://localhost:8080/admin
會根據sessionId,只要不換瀏覽器不清快取,不退出登入,session不過期。那麼這個sessionId有效,瀏覽器發起請求會根據這個sessionId從快取中取出session。再呼叫
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
再進行登入判斷:
return subject.getPrincipal() != null;
返回ture。跳到請求頁面。

上篇和這一篇,相信大家應該很請求shiro這個認證過程如何實現。無非就是根據sessionId去快取中取session,判斷是否已經登入,登入返回請求頁面。失敗進入登入頁面。

登入成功儲存session到快取中,下次使用者即取出已經登入的session
了。

下篇著重講解,shiro如何使用ehcache快取的。做個簡單例子,用一個map做個類似的簡單快取進行替換。

下下篇就用redis進行替換,解決session跨域問題。


菜鳥不易,望有問題指出,共同進步。