shiro 登陸成功後subject依然為空
shiro框架是一個強大的輕量級java安全框架。它提供了許可權驗證、加密、session管理的功能。shiro易用、上手快,應用場景大到企業級應用、小到手機應用都可以使用。本文就針對shiro的subject一個點展開,講講這個subject的來龍去脈。
我關注這個類要從一次錯誤說起。在我的專案裡面突然就出現subject無法獲得principals欄位資訊的情況,自然我每次登陸再請求什麼都是subject.getPrincipal()等於空。
SecurityUtils.getSubject()這個方法是從執行緒獲取的資料。在不瞭解subject原理的時候我的判斷是執行緒號換了所以資料就找不到了。所以,我一直在研究為啥執行緒號總換。這個思路是非常錯誤的,錯誤在並沒有真正瞭解subject這個類裡面的資料是怎麼來的。
那麼subject裡面的資料究竟是怎麼來的,怎麼就能從執行緒級別獲取到subject了呢?
我們在使用shiro的時候首先配置了一個它的代理過濾器在web.xml裡面。所以要從shiro的過濾器開始說起,shiro的內部過濾器的實現在這段程式碼。
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);
}
}
shiro過濾器第一步就將servletRequest、servletResponse兩個資料包裝成shiro型別的request和response。
第二步就是建立subject。
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
這個方法包括兩個部分:
1、獲取核心類securityManager 。
2、使用創造者模式建立subject。
2.1、Builder方法將securityManager、request、response屬性設定到subjectContext中。
2.2、呼叫buildWebSubject()方法做具體的建立。
public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();//1
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;
}
看下標註1的實現
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);//1.1
}
1.1具體實現如下:
public Subject createSubject(SubjectContext subjectContext) {
//獲取subjectContext資訊到context
SubjectContext context = copy(subjectContext);
//設定securityManager到context
context = ensureSecurityManager(context);
//設定cotext的session資訊到context
context = resolveSession(context);
//設定principals資訊到context
context = resolvePrincipals(context);
//建立subject
Subject subject = doCreateSubject(context);
//儲存subject 的登陸資訊儲存到session中或者持久化庫中
save(subject);
return subject;
}
從建立subject步驟來看subject資料應該是從context裡面獲取到的。具體怎麼獲取的呢?
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);
}
原來是subjectFacotry方法中建立的WebDelegatingSubject例項。也就是說subject裡面的各個欄位都是從這個方法裡面獲得的。下面我們就來看看我遇到的那個問題,pricipals怎麼為空了?資料應該從哪裡來的。
public PrincipalCollection resolvePrincipals() {
//MapContext的backingMap是否存在principals
PrincipalCollection principals = getPrincipals();
//MapContext的backingMap是否存在info,如果存在在這裡獲取。
if (CollectionUtils.isEmpty(principals)) {
//check to see if they were just authenticated:
AuthenticationInfo info = getAuthenticationInfo();
if (info != null) {
principals = info.getPrincipals();
}
}
//MapContext的backingMap是否存在subject,如果存在在這裡獲取。
if (CollectionUtils.isEmpty(principals)) {
Subject subject = getSubject();
if (subject != null) {
principals = subject.getPrincipals();
}
}
//MapContext的backingMap是否存在session,如果存在從session裡面獲取
if (CollectionUtils.isEmpty(principals)) {
//try the session:
Session session = resolveSession();
if (session != null) {
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
}
}
return principals;
}
從principals的獲取順序可以猜測principals這個資料應首先出現在session中。這樣如果在系統尚未登入時候,session剛剛建立,表單的資訊應該先放在session中,這樣我們就能獲得這個principals資料了。
接下來,我們從登入的過程開始看看資料是如何被放入session中的。
我們在登陸的時候會配置一個CustomFormAuthenticationFilter過濾器例項,如下:
>
/user/login=authc
/** =sysUser,onlineSession,,perms,roles
它的父類FormAuthenticationFilter。這個類是一個切面過濾器AccessControlFilter的子類。每一次請求都會首先執行該方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
isAccessAllowed(request, response, mappedValue)是一個空方法。onAccessDenied(request, response, mappedValue)方法在FormAuthenticationFilter中被實現。
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected.Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication.Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
該方法首先判斷請求路徑和我們xml配置的登陸路徑是否一致。然後判斷請求是否是post方法。滿足以上兩個條件呼叫父類的executeLogin(request, response)執行登陸操作。由此,我們看出登陸這個shiro已經為我們封裝好了,不需要我們自己寫。
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
executeLogin方法就做了三個事情:
1、將我們提交的表單資料封裝成token
2、從request、response裡面獲取subject
3、執行subject的login方法。
4、按照我們配置的跳轉路徑或者預設的路徑跳轉到登陸成功頁面。
第2步最終還是走了DefaultSecurityManager類的createSubject方法。這個時候由於是沒有登陸,那麼subject的pricipals、session欄位自然是空的。重點來看第3步
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
//3.1
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;
}
}
注意下這個方法在DelegatingSubject類裡面。所以這個方法作用就是填充subject。重點在程式碼中標註的3.1裡面。
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;
}
首先是校驗我們表單提交過來的資訊是否能夠登陸到系統中。
程式碼太多不貼出,寫下呼叫順序:
AuthenticatingSecurityManager-》AbstractAuthenticator-》ModularRealmAuthenticator-》AuthenticatingRealm-》MyRealm(自定義)
這時候如果在我們自定義的MyRealm校驗通過,就會返回一個
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
有了這些資訊就能將subject的相應的登陸資訊欄位資訊填充到subjectContext物件中,有了所有的資料再次呼叫createSubject(context)方法,重新建立subject例項。
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}
最後一件比較重要的事情就是session資訊的填充。session是什麼時候建立,並跟隨request裡的sessionid到瀏覽器,然後又是如何從session中恢復subject中的呢?
無論是否成功登陸了,session在shiro過濾器的時候就已經有了,如圖。
參見這段程式碼:
final Subject subject = createSubject(request, response);
建立subject的過程,不僅僅是要從session中恢復一些資料,如果系統尚不存在session的時候會主動建立。這個建立過程是從cookie的sessionid中建立。首次沒有session資訊的時候,會根據cookie帶過來的sessionId建立一個新的session。
類:DefaultWebSessionManager
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));
}
shiro配置的cookie會自動的帶回來一個數字串,這個數字串就是我們新建session的id值。
DefaultSessionManager裡面的retrieveSessionFromDataSource方法會從我們配置的sessionDAO中獲取持久化的session裡面是否有id為它的session資訊。如果沒有在我們持久化的sessionDAO中找到相應的session資訊,在debug下會列印我們經常看到的一個異常資訊:
org.apache.shiro.session.UnknownSessionException: There is no session with id [63916bfc-173c-4d39-a154-ae7c8f81a925]
at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170) ~[shiro-core-1.2.3.jar:1.2.3]
at org.apache.shiro.session.mgt.eis.CachingSessionDAO.readSession(CachingSessionDAO.java:261) ~[shiro-core-1.2.3.jar:1.2.3]
at org.apache.shiro.session.mgt.DefaultSessionManager.retrieveSessionFromDataSource(DefaultSessionManager.java:236) ~[shiro-core-1.2.3.jar:1.2.3]
由此我們知道,session資訊無論是否是新的還是已登入的session。在過濾器首次建立subject的時候都將session設定到了subject中。同時,subject資訊也會被放置到session中。
類:DefaultSecurityManager
save(subject);
類DefaultSubjectDAO
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;
}
那麼session中如何將principal放置到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);
}
當然,必須是在subject裡面含有pricipal資訊的時候才能夠放置成功。
回到登陸的過程,登陸的過程最終還是呼叫了DefaultSecurityManager類裡面的createSubject(SubjectContextsubjectContext)方法。由於在登陸的過程中一些登陸資訊被設定。
到了subjectContext中,這樣在呼叫完createSubject方法,登陸資訊會在createSubject(SubjectContextsubjectContext)方法呼叫 save(subject);時候被設定到sessoin。
由此,我們可以得出一個結論:subject裡面的登陸資訊每次從執行緒獲取之前,資料一定是從session中獲取。所以cookie的配置正確與否會影響到subject資料的正常顯示。cookie配置一定要注意兩個引數:path和domain。不要把path配置的太深,會導致有些路徑獲取不到cookie導致subject資料讀取失敗。不要把domain配置成跨域,跨域會導致cookie獲取不到。從而無法讀到sessionid而獲取不到session資訊。