暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

shiro中改造成restful无状态服务的DisabledSessionException问题分析与解决

开发架构二三事 2019-11-28
1001

关于 shiro 改造部分,之前有写文章专门讲过流程,长话短说,直接进入主题。

改造部分

SecurityManager 的配置如下:

  1. @Bean

  2. public SecurityManager securityManager(OAuthRealm oAuthRealm) {

  3. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

  4. securityManager.setRealm(oAuthRealm);

  5. securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());

  6. securityManager.setCacheManager(getEhCache());

  7. //securityManager.setSessionManager(sessionManager);

  8. // 自定义缓存实现 使用redis

  9. //securityManager.setCacheManager(cacheManager());

  10. return securityManager;

  11. }

StatelessDefaultSubjectFactory 的代码为:

  1. public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

  2. @Override

  3. public Subject createSubject(SubjectContext context) {

  4. //不创建session

  5. context.setSessionCreationEnabled(false);

  6. return super.createSubject(context);

  7. }

  8. }

DisabledSessionException

运行后,在调用 subject.login(token)方法时报错,报错信息如下:

  1. 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:

  1. public void login(AuthenticationToken token) throws AuthenticationException {

  2. clearRunAsIdentitiesInternal();

  3. Subject subject = securityManager.login(this, token);


  4. PrincipalCollection principals;


  5. String host = null;


  6. ...........

  7. }

这里我们主要关注下 securityManager.login(this, token)即 org.apache.shiro.mgt.DefaultSecurityManager#login:

  1. public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {

  2. AuthenticationInfo info;

  3. try {

  4. info = authenticate(token);

  5. } catch (AuthenticationException ae) {

  6. try {

  7. onFailedLogin(token, ae, subject);

  8. } catch (Exception e) {

  9. if (log.isInfoEnabled()) {

  10. log.info("onFailedLogin method threw an " +

  11. "exception. Logging and propagating original AuthenticationException.", e);

  12. }

  13. }

  14. throw ae; //propagate

  15. }


  16. Subject loggedIn = createSubject(token, info, subject);


  17. onSuccessfulLogin(token, info, loggedIn);


  18. return loggedIn;

  19. }

进入 createSubject:

  1. protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {

  2. SubjectContext context = createSubjectContext();

  3. context.setAuthenticated(true);

  4. context.setAuthenticationToken(token);

  5. context.setAuthenticationInfo(info);

  6. if (existing != null) {

  7. context.setSubject(existing);

  8. }

  9. return createSubject(context);

  10. }

再看 createSubject(context):

  1. public Subject createSubject(SubjectContext subjectContext) {

  2. //create a copy so we don't modify the argument's backing map:

  3. SubjectContext context = copy(subjectContext);


  4. //ensure that the context has a SecurityManager instance, and if not, add one:

  5. context = ensureSecurityManager(context);


  6. //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before

  7. //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the

  8. //process is often environment specific - better to shield the SF from these details:

  9. context = resolveSession(context);


  10. //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first

  11. //if possible before handing off to the SubjectFactory:

  12. context = resolvePrincipals(context);


  13. Subject subject = doCreateSubject(context);


  14. //save this subject for future reference if necessary:

  15. //(this is needed here in case rememberMe principals were resolved and they need to be stored in the

  16. //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).

  17. //Added in 1.2:

  18. save(subject);


  19. return subject;

  20. }

到了这里,我们主要关注以下两点:

  • doCreateSubject(context):

  1. protected Subject doCreateSubject(SubjectContext context) {

  2. return getSubjectFactory().createSubject(context);

  3. }

这里会调用 StatelessDefaultSubjectFactory 的 createSubject 方法,创建的是那个禁用 session 的 subject。

  • save(subject):

  1. protected void save(Subject subject) {

  2. this.subjectDAO.save(subject);

  3. }


  4. org.apache.shiro.mgt.DefaultSubjectDAO#save:


  5. public Subject save(Subject subject) {

  6. if (isSessionStorageEnabled(subject)) {

  7. saveToSession(subject);

  8. } else {

  9. log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +

  10. "authentication state are expected to be initialized on every request or invocation.", subject);

  11. }


  12. return subject;

  13. }


  14. protected boolean isSessionStorageEnabled(Subject subject) {

  15. return getSessionStorageEvaluator().isSessionStorageEnabled(subject);

  16. }


  17. public SessionStorageEvaluator getSessionStorageEvaluator() {

  18. return sessionStorageEvaluator;

  19. }



  20. org.apache.shiro.mgt.DefaultSessionStorageEvaluator#isSessionStorageEnabled(org.apache.shiro.subject.Subject):

  21. public boolean isSessionStorageEnabled(Subject subject) {

  22. return (subject != null && subject.getSession(false) != null) || isSessionStorageEnabled();

  23. }

这里我索性把代码都放在一起,方便查看。可以看到默认用的是 DefaultSessionStorageEvaluator,它的 isSessionStorageEnabled 的结果为 true,从而会进入到 saveToSession(subject)方法,我们继续来看这个方法:

  1. protected void saveToSession(Subject subject) {

  2. //performs merge logic, only updating the Subject's session if it does not match the current state:

  3. mergePrincipals(subject);

  4. mergeAuthenticationState(subject);

  5. }


  6. protected void mergePrincipals(Subject subject) {

  7. //merge PrincipalCollection state:


  8. PrincipalCollection currentPrincipals = null;


  9. //SHIRO-380: added if/else block - need to retain original (source) principals

  10. //This technique (reflection) is only temporary - a proper long term solution needs to be found,

  11. //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible

  12. //

  13. //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +

  14. if (subject.isRunAs() && subject instanceof DelegatingSubject) {

  15. try {

  16. Field field = DelegatingSubject.class.getDeclaredField("principals");

  17. field.setAccessible(true);

  18. currentPrincipals = (PrincipalCollection)field.get(subject);

  19. } catch (Exception e) {

  20. throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);

  21. }

  22. }

  23. if (currentPrincipals == null || currentPrincipals.isEmpty()) {

  24. currentPrincipals = subject.getPrincipals();

  25. }


  26. Session session = subject.getSession(false);


  27. if (session == null) {

  28. if (!isEmpty(currentPrincipals)) {

  29. session = subject.getSession();

  30. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);

  31. }

  32. // otherwise no session and no principals - nothing to save

  33. } else {

  34. PrincipalCollection existingPrincipals =

  35. (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);


  36. if (isEmpty(currentPrincipals)) {

  37. if (!isEmpty(existingPrincipals)) {

  38. session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);

  39. }

  40. // otherwise both are null or empty - no need to update the session

  41. } else {

  42. if (!currentPrincipals.equals(existingPrincipals)) {

  43. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);

  44. }

  45. // otherwise they're the same - no need to update the session

  46. }

  47. }

  48. }

上面的代码比较长,我们主要关注下 session = subject.getSession();即 org.apache.shiro.subject.support.DelegatingSubject#getSession():

  1. public Session getSession() {

  2. return getSession(true);

  3. }


  4. public Session getSession(boolean create) {

  5. if (log.isTraceEnabled()) {

  6. log.trace("attempting to get session; create = " + create +

  7. "; session is null = " + (this.session == null) +

  8. "; session has id = " + (this.session != null && session.getId() != null));

  9. }


  10. if (this.session == null && create) {


  11. //added in 1.2:

  12. if (!isSessionCreationEnabled()) {

  13. String msg = "Session creation has been disabled for the current subject. This exception indicates " +

  14. "that there is either a programming error (using a session when it should never be " +

  15. "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +

  16. "for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " +

  17. "for more.";

  18. throw new DisabledSessionException(msg);

  19. }


  20. log.trace("Starting session for host {}", getHost());

  21. SessionContext sessionContext = createSessionContext();

  22. Session session = this.securityManager.start(sessionContext);

  23. this.session = decorate(session);

  24. }

  25. return this.session;

  26. }

到这里你应该是看到异常出现的地方了。这里由于时间关系,就不再具体去分析了。

解决办法

securityManager:

  1. @Bean

  2. public SecurityManager securityManager(OAuthRealm oAuthRealm) {

  3. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

  4. securityManager.setRealm(oAuthRealm);

  5. securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());

  6. securityManager.setCacheManager(getEhCache());

  7. SubjectDAO subjectDAO = securityManager.getSubjectDAO();

  8. if (subjectDAO instanceof DefaultSubjectDAO){

  9. DefaultSubjectDAO defaultSubjectDAO = (DefaultSubjectDAO) subjectDAO;

  10. SessionStorageEvaluator sessionStorageEvaluator = defaultSubjectDAO.getSessionStorageEvaluator();

  11. if (sessionStorageEvaluator instanceof DefaultSessionStorageEvaluator){

  12. DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = (DefaultSessionStorageEvaluator) sessionStorageEvaluator;

  13. defaultSessionStorageEvaluator.setSessionStorageEnabled(false);

  14. }

  15. }

  16. //securityManager.setSessionManager(sessionManager);

  17. // 自定义缓存实现 使用redis

  18. //securityManager.setCacheManager(cacheManager());

  19. return securityManager;

  20. }


文章转载自开发架构二三事,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论