shiro中改造成restful无状态服务的DisabledSessionException问题分析与解决
- 2019 年 12 月 2 日
- 筆記
关于 shiro 改造部分,之前有写文章专门讲过流程,长话短说,直接进入主题。
改造部分
SecurityManager 的配置如下:
@Bean public SecurityManager securityManager(OAuthRealm oAuthRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(oAuthRealm); securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory()); securityManager.setCacheManager(getEhCache()); //securityManager.setSessionManager(sessionManager); // 自定义缓存实现 使用redis //securityManager.setCacheManager(cacheManager()); return securityManager; }
StatelessDefaultSubjectFactory 的代码为:
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { //不创建session context.setSessionCreationEnabled(false); return super.createSubject(context); }}
DisabledSessionException
运行后,在调用 subject.login(token)方法时报错,报错信息如下:
org.apache.shiro.subject.support.DisabledSessionException: 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 org.apache.shiro.subject.support.DisabledSessionException JavaDoc for more.
分析
这里不去描述太多,直接从源码一步步来看: org.apache.shiro.subject.support.DelegatingSubject#login:
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; ........... }
这里我们主要关注下 securityManager.login(this, token)即 org.apache.shiro.mgt.DefaultSecurityManager#login:
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; }
进入 createSubject:
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); }
再看 createSubject(context):
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):
protected Subject doCreateSubject(SubjectContext context) { return getSubjectFactory().createSubject(context); }
这里会调用 StatelessDefaultSubjectFactory 的 createSubject 方法,创建的是那个禁用 session 的 subject。
- save(subject):
protected void save(Subject subject) { this.subjectDAO.save(subject); } org.apache.shiro.mgt.DefaultSubjectDAO#save: 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; } org.apache.shiro.mgt.DefaultSessionStorageEvaluator#isSessionStorageEnabled(org.apache.shiro.subject.Subject): public boolean isSessionStorageEnabled(Subject subject) { return (subject != null && subject.getSession(false) != null) || isSessionStorageEnabled(); }
这里我索性把代码都放在一起,方便查看。可以看到默认用的是 DefaultSessionStorageEvaluator,它的 isSessionStorageEnabled 的结果为 true,从而会进入到 saveToSession(subject)方法,我们继续来看这个方法:
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 } } }
上面的代码比较长,我们主要关注下 session = subject.getSession();即 org.apache.shiro.subject.support.DelegatingSubject#getSession():
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; }
到这里你应该是看到异常出现的地方了。这里由于时间关系,就不再具体去分析了。
解决办法
securityManager:
@Bean public SecurityManager securityManager(OAuthRealm oAuthRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(oAuthRealm); securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory()); securityManager.setCacheManager(getEhCache()); SubjectDAO subjectDAO = securityManager.getSubjectDAO(); if (subjectDAO instanceof DefaultSubjectDAO){ DefaultSubjectDAO defaultSubjectDAO = (DefaultSubjectDAO) subjectDAO; SessionStorageEvaluator sessionStorageEvaluator = defaultSubjectDAO.getSessionStorageEvaluator(); if (sessionStorageEvaluator instanceof DefaultSessionStorageEvaluator){ DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = (DefaultSessionStorageEvaluator) sessionStorageEvaluator; defaultSessionStorageEvaluator.setSessionStorageEnabled(false); } } //securityManager.setSessionManager(sessionManager); // 自定义缓存实现 使用redis //securityManager.setCacheManager(cacheManager()); return securityManager; }