Shiro的session與subject的創建
shiro框架默認有3種會話管理的實現
- DefaultSessionManager 用於JavaSE環境
- ServletContainerSessionManager 用於Web環境,直接使用Servlet容器會話
- DefaultWebSessionManager 用於Web環境,自己維護會話,不會使用Servlet容器的會話管理
先看一下shiro的session的api,熟悉一下用法
//獲取當前subject
Subject subject = SecurityUtils.getSubject();
//通過subject獲取session
Session session = subject.getSession();
//這個參數用於判定會話不存在時是否創建新會話。
Session session = subject.getSession(true);
//獲取會話的唯一標識
session.getId();
//獲取主機地址
session.getHost();
//獲取超時時間
session.getTimeout();
//設置超時時間
session.setTimeout(毫秒);
//獲取會話的啓動時間
session.getStartTimestamp();
//獲取會話的最後訪問時間
session.getLastAccessTime();
//更新會話最後訪問時間
session.touch();
//銷燬session會話
session.stop();
//放入對象
session.setAttribute("key", "123");
//移除對象
session.removeAttribute("key");
上面的3種會話管理,前兩種不太常用,這裏我學習一下第三種比較常用的DefaultWebSessionManager
Shiro的入口是過濾器,上圖是shiro的過濾器類結構圖
在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);
}
}
}
繼續跟蹤源碼
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
//發現這裏包裝了request
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
//發現這裏包裝了response
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
//這裏創建了subjec
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);
}
}
再次跟蹤createSubject,查看創建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:
//session在這裏完成了創建
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在這裏創建了,並且和session關聯了起來
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
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
//注意這裏獲取到了session
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);
}
跟蹤WebDelegatingSubject方法
//since 1.2
public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
if (securityManager == null) {
throw new IllegalArgumentException("SecurityManager argument cannot be null.");
}
this.securityManager = securityManager;
this.principals = principals;
this.authenticated = authenticated;
this.host = host;
if (session != null) {
//把session賦值給了當前subject
this.session = decorate(session);
}
this.sessionCreationEnabled = sessionCreationEnabled;
}
總結一下:
- 每次請求都會創建一個subject,每次創建的時候,會先從會話中獲取所有的認證信息,包括登錄狀態,這樣就不會丟失會話狀態了
- session和subject創建的過程中完成了關聯
- 集羣環境下使用shiro,如果負載均衡策略不是ip_hash的,會重新登錄,因爲會話保持還是基於session的,除非我們重寫一下shiro的SessionDao,結合redis,就可以做到shiro集羣部署