shiro中session實現的簡單分析
前陣子對shiro進行分散式環境下的改造時跟了一遍原始碼,當時只是使用了思維帶圖簡要的記錄了一下方法的呼叫過程。最近有空了決定用部落格詳細的記錄分析一下這個流程,以幫助自己更好的理解。
配置
首先看看shiro在web.xml檔案中的配置
<!-- shiro過濾器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class >
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
可以看到使用的<filter-class>標籤是Spring的代理過濾器,那麼它是如何代理shiro的過濾器的呢?看看DelegatingFilterProxy的原始碼
@Override
protected void initFilterBean () throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//如果沒有delegate則根據<filter-name>去Spring容器中尋找對應的bean
if (this.targetBeanName == null ) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
於是Spring中應該配置了name為shiroFilter的bean,下面看看Spring中與shiro相關的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- shiroFilter物件 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/user/loginpage"/>
<property name="unauthorizedUrl" value="/403.html"/>
<property name="filterChainDefinitions">
<value>
/user/loginpage = anon
/user/login = anon
/* = authc
/user/perms1 = perms["user:delete"]
/user/perms2 = perms["user:select"]
/user/admin = roles["admin"]
#自定義的過濾器,只要多個許可權中有一個滿足即可
/user/users = rolesOr["admin","user"]
</value>
</property>
<property name="filters">
<map>
<entry key="rolesOr" value-ref="rolesOrFilter" />
</map>
</property>
</bean>
<!-- 建立securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"></property>
<property name="sessionManager" ref="sessionManager"></property>
</bean>
<!-- 建立realm -->
<bean id="realm" class="com.cfh.studyshiro.common.CustomeRealm">
</bean>
<!-- 自定義過濾器 -->
<bean id="rolesOrFilter" class="com.cfh.studyshiro.filter.RolesOrFilter" />
<!-- 注入自定義的sessionDao -->
<bean id="redisSessionDao" class="com.cfh.studyshiro.common.RedisSessionDao" />
<!-- 在sessionManager中引入自定義的sessionDao -->
<bean id="sessionManager" class="com.cfh.studyshiro.common.CustomSessionManager">
<property name="sessionDAO" ref="redisSessionDao" />
<!-- 關閉cookie -->
<!-- <property name="sessionIdCookieEnabled" value="false" /> -->
</bean>
</beans>
ShiroFilterFactoryBean的原始碼這裡不進行討論,先看看ShiroFilterFactoryBean.class中生成ShiroFilter的createInstance()方法
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
spring的DelegatingFilterProxy由此獲得了對AbstractShiroFilter的代理。下面我們在DelegatingFilterProxy的doFilter方法上打上斷點,跟蹤shiro在一次登入請求中都會做哪些處理。
斷點除錯
1.入口:invokeDelegate(delegateToUse, request, response, filterChain)
// Let the delegate perform the actual doFilter operation.
2.接著執行OncePerRequestFilter的doFilter方法
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
可以看到這個方法會先判斷請求是否是過濾的了,在最後一個分支呼叫了doFilterInternal(request, response, filterChain);這個方法,我們跟進方法中看看。
3.斷點跳進了AbstractShiroFilter中,觀察這個類,發現他繼承了OncePerRequestFilter 並重寫了其中的doFilterInternal
public abstract class AbstractShiroFilter extends OncePerRequestFilter
接下來看看doFilterInternal中的邏輯
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
主要的邏輯就是建立一個subject,並在建立完成後非同步執行一個callable任務用於更新 updateSessionLastAccessTime。接下里看看subject的建立過程。
4.createSubject()
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
使用建造者模式建造了一個WebSubject物件,繼續跟進
Builder的構造方法
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
super(securityManager);
if (request == null) {
throw new IllegalArgumentException("ServletRequest argument cannot be null.");
}
if (response == null) {
throw new IllegalArgumentException("ServletResponse argument cannot be null.");
}
setRequest(request);
setResponse(response);
}
build方法
public WebSubject buildWebSubject() {
//呼叫父類的buildSubject()
Subject subject = super.buildSubject();
if (!(subject instanceof WebSubject)) {
String msg = "Subject implementation returned from the SecurityManager was not a " +
WebSubject.class.getName() + " implementation. Please ensure a Web-enabled SecurityManager " +
"has been configured and made available to this builder.";
throw new IllegalStateException(msg);
}
return (WebSubject) subject;
}
可以看到WebSubject的build方法最終呼叫了父類的buildSubject方法,跟進這個方法。
5.跟進父類Subject的buildSubject方法
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
發現呼叫的是securityManager的createSubject方法,繼續跟進
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;
}
方法的的作用註釋已經說明的很清楚,我們需要注意的是doCreateSubject(context)這個方法,securityManger通過這個方法根據傳入的subjectContext構建了一個Subject物件。
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
跟進發現securityManger使用了內部的subjectFactoy物件進行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);
}
方法首先對傳入的SubjectContext的型別做了一個判斷,我們只關心wsc的情況。發現方法通過wsc獲取了shiro框架中一系列重要物件如principal,session後構建了一個WebDelegatingSubject物件。先看看resolveSession這個方法。
6.SecurityManaget中的resolveSession
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
log.debug("Context already contains a session. Returning.");
return context;
}
try {
//Context couldn't resolve it directly, let's see if we can since we have direct access to
//the session manager:
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +
"(session-less) Subject instance.", e);
}
return context;
}
首先試圖從SubjectContext中獲取session,因此讓我跟進一下這個方法:
sc中的resolveSession方法
//SubjectContext.class
public Session resolveSession() {
Session session = getSession();
if (session == null) {
//try the Subject if it exists:
Subject existingSubject = getSubject();
if (existingSubject != null) {
session = existingSubject.getSession(false);
}
}
return session;
}
首先會檢查subjectContext中的session是否是null。因為此時session還沒有與sc做繫結因此getSession方法必定返回null,跳入第二個分支試圖從與sc繫結的subject中獲取,同理此時subject也為null,因此return session 返回的一定是一個null物件。讓我們回到securityManager中的resolveSession方法,接下來會執行session為null的那一個分支。即
try {
//Context couldn't resolve it directly, let's see if we can since we have direct access to
//the session manager:
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +
"(session-less) Subject instance.", e);
}
這個程式碼片段。通過resolveContextSession(context)方法獲取session並在獲取成功之後與context進行繫結(因此接下來如果再呼叫這個方法可以直接走從Context獲取的分支)。於是我們分析的重點就轉移到了resolveContextSession(context)這個方法上。
7.resolveContextSession(context)
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
SessionKey key = getSessionKey(context);
if (key != null) {
return getSession(key);
}
return null;
}
這裡通過context獲取了一個新的物件SessionKey,只有一個方法getSessionId(),通過註釋可以得知通過SessionKey可以找到唯一指定的session。弄清楚SessionKey的作用後我們開始分析getSessionKey方法
//DefaultWebSecurityManager.class
protected SessionKey getSessionKey(SubjectContext context) {
//首先判斷是否是web環境
if (WebUtils.isWeb(context)) {
//獲取sessionId
Serializable sessionId = context.getSessionId();
ServletRequest request = WebUtils.getRequest(context);
ServletResponse response = WebUtils.getResponse(context);
return new WebSessionKey(sessionId, request, response);
} else {
return super.getSessionKey(context);
}
}
context.getSessionId()
public Serializable getSessionId() {
return getTypedValue(SESSION_ID, Serializable.class);
}
從context中根據鍵SESSION_ID進行取值,因為還沒有進行設定因此返回的sessionId為null。
getSessionKey最終構造了一個WebSessionKey物件並返回。因此resolveContextSession方法走入執行getSession(key)方法的分支。
getSession(key)
//SessionSecurityManager
public Session getSession(SessionKey key) throws SessionException {
return this.sessionManager.getSession(key);
}
繼續跟進SessionManager的getSession(key)
//AbstractNativeSessionManager.class
public Session getSession(SessionKey key) throws SessionException {
//首先根據key尋找session
Session session = lookupSession(key);
return session != null ? createExposedSession(session, key) : null;
}
lookupSession(key)
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
}
return doGetSession(key);
}
doGetSession(key)
@Override
protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
enableSessionValidationIfNecessary();
log.trace("Attempting to retrieve session with key {}", key);
Session s = retrieveSession(key);
if (s != null) {
validate(s, key);
}
return s;
}
重點關注retrieveSession(key);這個方法,使用該方法獲取session後使用validate方法校驗後即可返回。
retrieveSession(key)
//這裡使用的是我在CustomeSessionManager中覆寫的retrieveSession
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//通過SessionKey物件獲取sessionId
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
//嘗試從request中取而不是每次都請求資料庫
if(request !=null && sessionId !=null){
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
//如果request中沒有session則從資料庫中請求並把請求結果設定給sessionKey
Session session = super.retrieveSession(sessionKey);
if(request !=null && sessionId != null){
request.setAttribute(sessionId.toString(),session);
}
return session;
}
首先關注getSessionId(sessionKey)這個方法,因為到目前為止我們的sessionKey物件中的sessionId屬性仍然是空的。
getSessionId(sessionKey)
//DefaultWebSessionManager.class
public Serializable getSessionId(SessionKey key) {
Serializable id = super.getSessionId(key);
if (id == null && WebUtils.isWeb(key)) {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
id = getSessionId(request, response);
}
return id;
}
super.getSessionId(key);的邏輯很簡單,就是獲取傳入sessionKey的sessionId屬性,獲取到的當然是空值因此走入if分支。從sessionKey中獲取request和response物件然後通過getSessionId(request, response)方法生成sessionId。
getSessionId(request, response)
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return getReferencedSessionId(request, response);
}
getReferencedSessionId(request, response)
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
//試圖從cookie中獲取sessionId(這裡已經可以看出shiro的session實現原理也是基於cookie的)
String id = getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
} else {//cookie被禁用的情況,使用url字尾裹挾sessionId的方式實現session
//not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
//try the URI path segment parameters first:
id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
if (id == null) {
//not a URI path segment parameter, try the query parameters:
String name = getSessionIdName();
id = request.getParameter(name);
if (id == null) {
//try lowercase:
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
//automatically mark it valid here. If it is invalid, the
//onUnknownSession method below will be invoked and we'll remove the attribute at that time.
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
// always set rewrite flag - SHIRO-361
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return id;
}
getSessionIdCookieValue()
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
if (!isSessionIdCookieEnabled()) {
log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
return null;
}
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null.");
return null;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
}
readValue()
//SimpleCookie.class
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
String name = getName();
String value = null;
javax.servlet.http.Cookie cookie = getCookie(request, name);
if (cookie != null) {
// Validate that the cookie is used at the correct place.
String path = StringUtils.clean(getPath());
if (path != null && !pathMatches(path, request.getRequestURI())) {
log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path});
} else {
value = cookie.getValue();
log.debug("Found '{}' cookie value [{}]", name, value);
}
} else {
log.trace("No '{}' cookie value", name);
}
return value;
}
readValue()的邏輯很清晰不多做解釋
此時結果一番折騰終於獲得了sessionId,讓我們回到retrieveSession方法繼續往下執行
Session session = super.retrieveSession(sessionKey);
super.retrieveSession(sessionKey)
//DefaultWebSessionManager.class
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//雖然此時sessionKey中的sessionId依然是null但由於我們在
//getReferencedSessionId方法中獲取到sessionId後將sessionId存在了
//request物件中,因此sessionId的值是從request中獲取的
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a " +
"session could not be found.", sessionKey);
return null;
}
//根據sessionId去資料來源取相應的session
Session s = retrieveSessionFromDataSource(sessionId);
if (s == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
return s;
}
這裡需要重視 Session s = retrieveSessionFromDataSource(sessionId)這句程式碼,通過sessionId去相應的資料來源獲取對應的session,跟進一下。
retrieveSessionFromDataSource(sessionId)
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return sessionDAO.readSession(sessionId);
}
可以看出呼叫了SessionDAO的readSession方法,由於sessionDAO是可以自由定義與替換的,所以我們可以根據實際場景更換相應的SessionDao。那麼到這了就取得了session。可以看到shiro框架內部自身實現了一套session機制,因此shiro的session是可以脫離web容器使用的。
總結
我們從一次請求開始簡單分析了shiro框架對於session的處理流程,下一篇部落格準備以同樣的模式分析shiro對於身份驗證以及許可權認證的處理流程。(最近比較懶散更新時間待定哈哈)。