1. 程式人生 > >shiro 登陸成功後subject依然為空

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資訊。

轉載:https://www.aliyun.com/jiaocheng/825309.html