shiro登陸流程原始碼詳解

lilingyan發表於2019-06-03

前言

抽取了shiro最基本的登陸流程(web登陸是基於這層開發的)。
詳解原始碼

建立一個AuthenticationToken進行登入。

SecurityUtils.getSecurityManager().login(null,authenticationToken);
複製程式碼

登陸主流程

 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        /**
         * 取獲校驗token的資訊
         * 如果有返回就認為登陸成功
         * 丟擲任何AuthenticationException子類錯誤 就認為登陸失敗
         */
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        throw ae;
    }
    /**
     * 走到這裡
     * 證明已經登陸成功
     * 
     * 下一步就是建立Subject
     */
    Subject loggedIn = createSubject(token, info, subject);
    return loggedIn;
}
複製程式碼

登陸身份校驗

public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    /**
     * 使用登陸器去登陸
     * 預設使用ModularRealmAuthenticator
     */
    return this.authenticator.authenticate(token);
}
複製程式碼
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        //只演示單realm的情況
//            return doMultiRealmAuthentication(realms, authenticationToken);
        return null;
    }
}
複製程式碼
/**
     * 只有一個realm的情況下使用
     * @param realm
     * @param token
     * @return
     */
    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    /**
     * 先判斷這個realm是否可以校驗這個token
     * 這個方法需要我們在實現自己的realm時重寫
     */
    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);
    }
    /**
     * 從我們自己的realm中獲取校驗後的登陸資訊
     */
    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;
}
複製程式碼

建立Subject

 /**
 * 登陸成功後建立Subject
 * 如果已經有subject
 * 則把現在的認證資訊與原來的Subject繫結
 * 如果沒有Subject  則建立一個 並執行繫結操作
 * @param token
 * @param info
 * @param existing
 * @return
 */
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
    SubjectContext context = createSubjectContext();
    context.setAuthenticated(true);             //增加了登陸成功的標誌
    /**
     * 同時儲存了登陸的token和realm認證後返回的資訊
     */
    context.setAuthenticationToken(token); 
    context.setAuthenticationInfo(info);
    if (existing != null) {
        context.setSubject(existing);
    }
    return createSubject(context);
}
複製程式碼
public Subject createSubject(SubjectContext subjectContext) {
    //複製了一遍subjectContext
    SubjectContext context = copy(subjectContext);
    //確儲存在SecurityManager
    context = ensureSecurityManager(context);
    //解析Session
    context = resolveSession(context);
    Subject subject = doCreateSubject(context);
    /**
     * 儲存subject
     * web情況下 會儲存在session中
     */
    save(subject);
    return subject;
}
複製程式碼
protected Subject doCreateSubject(SubjectContext context) {
    //使用Subject工廠統一建立Subject
    return getSubjectFactory().createSubject(context);
}
複製程式碼
public Subject createSubject(SubjectContext context) {
    SecurityManager securityManager = context.resolveSecurityManager();
    //解析session
    Session session = context.resolveSession();
    /**
     * session是否自動建立標記
     * ,沒開啟 會報錯 @DelegatingSubject$getSession(boolean create)
     */
    boolean sessionCreationEnabled = context.isSessionCreationEnabled();
    //realm中返回的使用者憑證資訊
    PrincipalCollection principals = context.resolvePrincipals();
    boolean authenticated = context.resolveAuthenticated();

    //建立
    return new DelegatingSubject(principals, authenticated, session, sessionCreationEnabled, securityManager);
}
複製程式碼

儲存subject

/**
 * 具體儲存邏輯
 * 由subjectDAO的實現類完成
 * @param subject
 */
protected void save(Subject subject) {
    this.subjectDAO.save(subject);
}
複製程式碼
/**
 * 預設的實現方式是把subject儲存到session中
 * @param subject
 * @return
 */
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;
}
複製程式碼
/**
 * 儲存登陸的資訊和登陸的狀態
 * @param subject
 */
protected void saveToSession(Subject subject) {
    mergePrincipals(subject);
    mergeAuthenticationState(subject);
}
複製程式碼
protected void mergePrincipals(Subject subject) {
    PrincipalCollection currentPrincipals = null;
    if (currentPrincipals == null || currentPrincipals.isEmpty()) {
        currentPrincipals = subject.getPrincipals();
    }

    Session session = subject.getSession(false);

    /**
     * 如果有Session
     * 則把變動的PrincipalCollection儲存進去
     */
    if (session == null) {
        if (!isEmpty(currentPrincipals)) {
            session = subject.getSession();
            session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
        }
    } else {
        PrincipalCollection existingPrincipals =
                (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);

        if (isEmpty(currentPrincipals)) {
            if (!isEmpty(existingPrincipals)) {
                session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            }
        } else {
            //只有修改過了的,才會被儲存
            if (!currentPrincipals.equals(existingPrincipals)) {
                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
            }
        }
    }
}
複製程式碼
/**
 * 重新整理session中的登陸標記
 * @param subject
 */
protected void mergeAuthenticationState(Subject subject) {

    Session session = subject.getSession(false);

    if (session == null) {
        if (subject.isAuthenticated()) {
            /**
             * 如果登陸成功,並且第一次訪問Session
             * 則會去建立也給新的
             */
            session = subject.getSession();
            session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
        }
    } else {
        /**
         * 如果有session
         * 則更新標記
         */
        Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);

        if (subject.isAuthenticated()) {
            if (existingAuthc == null || !existingAuthc) {
                session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
            }
        } else {
            if (existingAuthc != null) {
                session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
            }
        }
    }
}
複製程式碼

儲存subject時,如何生成的session(如果開啟)

protected Session createSession(SessionContext context) throws AuthorizationException {
    return doCreateSession(context);
}
複製程式碼
protected Session doCreateSession(SessionContext context) {
    /**
     * 使用工廠類建立session
     * 預設SimpleSession
     */
    Session s = newSessionInstance(context);
    create(s);
    return s;
}
複製程式碼
public Session createSession(SessionContext initData) {
    /**
     * 這裡生成sessionId 可以嗎?
     */
    return new SimpleSession();
}
複製程式碼

shiro預設使用MemorySessionDAO

protected void create(Session session) {
    if (log.isDebugEnabled()) {
        log.debug("Creating new EIS record for new session instance [" + session + "]");
    }
    //最終交由sessionDAO生成
    sessionDAO.create(session);
}
複製程式碼
protected Serializable doCreate(Session session) {
    /**
     * sessionId最終在SessionDAO中生成
     * 預設使用java的uuid
     */
    Serializable sessionId = generateSessionId(session);
    assignSessionId(session, sessionId);
    storeSession(sessionId, session);
    return sessionId;
}
複製程式碼

修改session時,shiro是如何同步到本地的(或者HttpSession)

/**
 * 如果沒有session
 * 則建立
 * @param create
 * @return
 */
@Override
public Session getSession(boolean create) {
    if (log.isTraceEnabled()) {
        log.trace("attempting to get session; create = " + create +
                "; session is null = " + (this.session == null) +
                "; session has id = " + (this.session != null && session.getId() != null));
    }

    if (this.session == null && create) {

        //added in 1.2:
        if (!isSessionCreationEnabled()) {      //沒開啟sessionCreationEnabled  會報錯
            String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
                    "that there is either a programming error (using a session when it should never be " +
                    "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
                    "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
                    "for more.";
            throw new DisabledSessionException(msg);
        }

        /**
         * 建立的是DefaultSessionContext
         * 預設沒什麼內容
         */
        SessionContext sessionContext = createSessionContext();
        /**
         * 最終交由securityManager來生成Session
         */
        Session session = this.securityManager.start(sessionContext);
        /**
         * 最終讓使用者操作的多次包裝的Session
         */
        this.session = decorate(session);
    }
    return this.session;
}
複製程式碼
 public Session start(SessionContext context) {
    /**.
     * 建立了一個SimpleSession
     */
    Session session = createSession(context);
    onStart(session, context);
    /**
     * 建立對外暴露的Session
     * 其實就是代理了一下
     *
     * 主要為了讓session在普通的操作下完成更多的功能
     */
    return createExposedSession(session, context);
}
複製程式碼
protected Session createExposedSession(Session session, SessionContext context) {
    //代理Session
    return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}
複製程式碼
/**
 * 代理的session
 * 他會把對所有對session的操作
 * 提交給sessionManager去處理
 * @Author: lilingyan
 * @Date 2019/6/3 13:37
 */
public class DelegatingSession implements Session {

    private final SessionKey key;
    private final transient NativeSessionManager sessionManager;
    
    @Override
    public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException {
        if (value == null) {
            removeAttribute(attributeKey);
        } else {
            //被代理了
            sessionManager.setAttribute(this.key, attributeKey, value);
        }
    }
    ...
複製程式碼
//在SessionManager中代理的處理方法
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
    sessionDAO.update(session);
}
複製程式碼

相關文章