前面的章節說完了Subject的創建過程,創建完Subject需要將其保存起來,而保存的過程則由SubjectDAO完成。既然每一次訪問都會創建一個新的Subject,那麼爲什麼還需要SubjectDAO來存儲呢?其實SubjectDAO做的事情很少,沒有存儲Subject,只是將當前Subject的authenticated(是否校驗成功)和principal(身份)放入到session中。
Subject的生命週期:
- 每一次訪問都會創建一個新的subject對象。
- 將創建的subject對象綁定到ThreadContext上,之後通過SecurityUtils便可以在程序的任何地方獲取到Subject對象。
- 每一次創建完成subject,都會通過SubjectDAO將subject屬性存放到session中去,但是隻存放principal(身份)和authenticated(是否校驗成功)。
org.apache.shiro.mgt.DefaultSecurityManager中createSubject創建完Subject,調用SubjectDAO保存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;
}
protected void save(Subject subject) {
this.subjectDAO.save(subject);
}
SubjectDAO是一個接口,主要包含save和delete兩個方法操作Subject,其主要實現類是DefaultSubjectDAO
public interface SubjectDAO {
/**
* Persists the specified Subject's state for later access. If there is a no existing state persisted, this
* persists it if possible (i.e. a create operation). If there is existing state for the specified {@code Subject},
* this method updates the existing state to reflect the current state (i.e. an update operation).
*
* @param subject the Subject instance for which its state will be created or updated.
* @return the Subject instance to use after persistence is complete. This can be the same as the method argument
* if the underlying implementation does not need to make any Subject changes.
*/
Subject save(Subject subject);
/**
* Removes any persisted state for the specified {@code Subject} instance. This is a delete operation such that
* the Subject's state will not be accessible at a later time.
*
* @param subject the Subject instance for which any persistent state should be deleted.
*/
void delete(Subject subject);
}
通過org.apache.shiro.mgt.DefaultSubjectDAO中的save()方法,通過判斷isSessionStorageEnabled()將subject的屬性保存到session中去。但是決定是否進行保存是由sessionStorageEvaluator這個參數進行控制的。
/**
* Saves the subject's state to the subject's {@link org.apache.shiro.subject.Subject#getSession() session} only
* if {@link #isSessionStorageEnabled(Subject) sessionStorageEnabled(subject)}. If session storage is not enabled
* for the specific {@code Subject}, this method does nothing.
* <p/>
* In either case, the argument {@code Subject} is returned directly (a new Subject instance is not created).
*
* @param subject the Subject instance for which its state will be created or updated.
* @return the same {@code Subject} passed in (a new Subject instance is not created).
*/
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 boolean isSessionStorageEnabled(Subject subject) {
return getSessionStorageEvaluator().isSessionStorageEnabled(subject);
}
public SessionStorageEvaluator getSessionStorageEvaluator() {
return sessionStorageEvaluator;
}
SessionStorageEvaluator其主要實現類是DefaultSessionStorageEvaluator
public interface SessionStorageEvaluator {
/**
* Returns {@code true} if the specified {@code Subject}'s
* {@link org.apache.shiro.subject.Subject#getSession() session} may be used to persist that Subject's
* state, {@code false} otherwise.
*
* @param subject the {@code Subject} for which session state persistence may be enabled
* @return {@code true} if the specified {@code Subject}'s
* {@link org.apache.shiro.subject.Subject#getSession() session} may be used to persist that Subject's
* state, {@code false} otherwise.
* @see Subject#getSession()
* @see Subject#getSession(boolean)
*/
boolean isSessionStorageEnabled(Subject subject);
}
決定策略就是通過DefaultSessionStorageEvaluator的sessionStorageEnabled的狀態和Subject是否存在Session決定的
public class DefaultSessionStorageEvaluator implements SessionStorageEvaluator {
private boolean sessionStorageEnabled = true;
public boolean isSessionStorageEnabled(Subject subject) {
return (subject != null && subject.getSession(false) != null) || isSessionStorageEnabled();
}
public boolean isSessionStorageEnabled() {
return sessionStorageEnabled;
}
public void setSessionStorageEnabled(boolean sessionStorageEnabled) {
this.sessionStorageEnabled = sessionStorageEnabled;
}
}
如果允許存儲subject的session,則執行以下的具體存儲過程:
/**
* Saves the subject's state (it's principals and authentication state) to its
* {@link org.apache.shiro.subject.Subject#getSession() session}. The session can be retrieved at a later time
* (typically from a {@link org.apache.shiro.session.mgt.SessionManager SessionManager} to be used to recreate
* the {@code Subject} instance.
*
* @param subject the subject for which state will be persisted to its session.
*/
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);
}
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 (!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 (isEmpty(currentPrincipals)) {
if (!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
}
}
}
protected void mergeAuthenticationState(Subject subject) {
Session session = subject.getSession(false);
if (session == null) {
if (subject.isAuthenticated()) {
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
//otherwise no session and not authenticated - nothing to save
} else {
Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
if (subject.isAuthenticated()) {
if (existingAuthc == null || !existingAuthc) {
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
//otherwise authc state matches - no need to update the session
} else {
if (existingAuthc != null) {
//existing doesn't match the current state - remove it:
session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
}
//otherwise not in the session and not authenticated - no need to update the session
}
}
}
上面重點關注,當subject.getSession(false)獲取的Session爲空時(它不會去創建Session),此時就需要去創建Session,subject.getSession()則默認調用的是subject.getSession(true),則會進行Session的創建。
public Session getSession() {
return getSession(true);
}
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()) {
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);
}
log.trace("Starting session for host {}", getHost());
SessionContext sessionContext = createSessionContext();
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
使用session來持久化保存Subject的principal(身份)和authenticated(是否校驗成功)。在Session中保存Subject信息後,通過cookie上送的sessionId,shiro便可以根據session來重建Subject實例。
用戶每次請求,均使用session重建subject對象,那麼我們可以修改session中的身份信息,以實現用戶非重新登錄,便可獲取到最新的身份信息principal。
源碼參考:org.apache.shiro.mgt.DefaultSubjectDAO#mergePrincipals
但是在無狀態認證中,請求每次攜帶token,每次都要經過身份認證。通常就不需要session來存儲此狀態。便可以禁止存儲,如下代碼,禁止session存儲subject信息:
@Bean
public DefaultSubjectDAO defaultSubjectDAO() {
return new DefaultSubjectDAO() {
@Override
protected boolean isSessionStorageEnabled(Subject subject) {
return false;
}
};
}
參考文章:https://www.jianshu.com/p/e2f5e231a2f7,https://blog.csdn.net/hxpjava1/article/details/77917106