继续上一篇文章的案例,第一次使用SecurityUtils.getSubject()来获取Subject时  

1. public static
2. Subject subject = ThreadContext.getSubject();
3. if (subject == null) {
4. new
5. ThreadContext.bind(subject);
6. }
7. return
8. }

  


使用ThreadLocal模式来获取,若没有则创建一个并绑定到当前线程。此时创建使用的是Subject内部类Builder来创建的,Builder会创建一个SubjectContext接口的实例DefaultSubjectContext,最终会委托securityManager来根据SubjectContext信息来创建一个Subject,下面详细说下该过程,在DefaultSecurityManager的createSubject方法中:

 

1. public
2. SubjectContext context = copy(subjectContext);
3.
4. context = ensureSecurityManager(context);
5.
6. context = resolveSession(context);
7.
8. context = resolvePrincipals(context);
9.
10. Subject subject = doCreateSubject(context);
11.
12. save(subject);
13.
14. return
15. }



首先就是复制SubjectContext,SubjectContext 接口继承了Map<String, Object>,然后加入了几个重要的SecurityManager、SessionId、Subject、PrincipalCollection、Session、boolean authenticated、boolean sessionCreationEnabled、Host、AuthenticationToken、AuthenticationInfo等众多信息。 


然后来讨论下接口设计: 


shiro源码分析(二)Subject和Session_lua

 



讨论1:首先是SubjectContext为什么要去实现Map<String, Object>? 

SubjectContext提供了常用的get、set方法,还提供了一个resolve方法,以SecurityManager为例:

 

这些get、set方法则用于常用的设置和获取,而resolve则表示先调用getSecurityManager,如果获取不到,则使用其他途径来获取,如DefaultSubjectContext的实现:

1. SecurityManager getSecurityManager();  
2.
3. void
4.
5. SecurityManager resolveSecurityManager();

 

1. public
2. SecurityManager securityManager = getSecurityManager();
3. if (securityManager == null) {
4. if
5. "No SecurityManager available in subject context map. "
6. "Falling back to SecurityUtils.getSecurityManager() lookup.");
7. }
8. try
9. securityManager = SecurityUtils.getSecurityManager();
10. catch
11. if
12. "No SecurityManager available via SecurityUtils. Heuristics exhausted.", e);
13. }
14. }
15. }
16. return
17. }



如果getSecurityManager获取不到,则使用SecurityUtils工具来获取。 

再如resolvePrincipals

 

1. public
2. PrincipalCollection principals = getPrincipals();
3.
4. if
5. //check to see if they were just authenticated:
6. AuthenticationInfo info = getAuthenticationInfo();
7. if (info != null) {
8. principals = info.getPrincipals();
9. }
10. }
11.
12. if
13. Subject subject = getSubject();
14. if (subject != null) {
15. principals = subject.getPrincipals();
16. }
17. }
18.
19. if
20. //try the session:
21. Session session = resolveSession();
22. if (session != null) {
23. principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
24. }
25. }
26.
27. return
28. }


普通的getPrincipals()获取不到,尝试使用其他属性来获取。 

讨论2:此时就有一个问题,有必要再对外公开getPrincipals方法吗?什么情况下外界会去调用getPrincipals方法而不会去调用resolvePrincipals方法? 


然后我们继续回到上面的类图设计上: 

DefaultSubjectContext继承了MapContext,MapContext又实现了Map<String, Object>,看下此时的MapContext有什么东西:

 

1. public class MapContext implements
2.
3. private static final long
4.
5. private final
6.
7. public
8. this.backingMap = new
9. }
10.
11. public
12. this();
13. if
14. this.backingMap.putAll(map);
15. }
16. }
17. //略
18. }



MapContext内部拥有一个类型为HashMap的backingMap属性,大部分方法都由HashMap来实现,然后仅仅更改某些行为,MapContext没有选择去继承HashMap,而是使用了组合的方式,更加容易去扩展,如backingMap的类型不一定非要选择HashMap,可以换成其他的Map实现,一旦MapContext选择继承HashMap,如果想对其他的Map类型进行同样的功能增强的话,就需要另写一个类来继承它然后改变一些方法实现,这样的话就会有很多重复代码。这也是设计模式所强调的少用继承多用组合。但是MapContext的写法使得子类没法去替换HashMap,哎,心塞

shiro源码分析(二)Subject和Session_Java_02

 。 

MapContext又提供了如下几个返回值不可修改的方法:

 

1. public
2. return
3. }
4.
5. public
6. return
7. }
8.
9. public
10. return
11. }



有点扯远了。继续回到DefaultSecurityManager创建Subject的地方:

 

1. public
2. //create a copy so we don't modify the argument's backing map:
3. SubjectContext context = copy(subjectContext);
4.
5. //ensure that the context has a SecurityManager instance, and if not, add one:
6. context = ensureSecurityManager(context);
7.
8. //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
9. //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
10. //process is often environment specific - better to shield the SF from these details:
11. context = resolveSession(context);
12.
13. //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
14. //if possible before handing off to the SubjectFactory:
15. context = resolvePrincipals(context);
16.
17. Subject subject = doCreateSubject(context);
18.
19. //save this subject for future reference if necessary:
20. //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
21. //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
22. //Added in 1.2:
23. save(subject);
24.
25. return
26. }



对于context,把能获取到的参数都凑齐,SecurityManager、Session。resolveSession尝试获取context的map中获取Session,若没有则尝试获取context的map中的Subject,如果存在的话,根据此Subject来获取Session,若没有再尝试获取sessionId,若果有了sessionId则构建成一个DefaultSessionKey来获取对应的Session。 

整个过程如下;

1. protected
2. if (context.resolveSession() != null) {
3. "Context already contains a session. Returning.");
4. return
5. }
6. try
7. //Context couldn't resolve it directly, let's see if we can since we have direct access to
8. //the session manager:
9. Session session = resolveContextSession(context);
10. if (session != null) {
11. context.setSession(session);
12. }
13. catch
14. "Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous "
15. "(session-less) Subject instance.", e);
16. }
17. return
18. }

先看下context.resolveSession():


1. public
2. //这里则是直接从map中取出Session
3. Session session = getSession();
4. if (session == null) {
5. //try the Subject if it exists:
6. //若果没有,尝试从map中取出Subject
7. Subject existingSubject = getSubject();
8. if (existingSubject != null) {
9. //这里就是Subject获取session的方法,需要详细看下
10. false);
11. }
12. }
13. return
14. }



existingSubject.getSession(false):通过Subject获取Session如下

 

1. public Session getSession(boolean
2. if
3. "attempting to get session; create = "
4. "; session is null = " + (this.session == null) +
5. "; session has id = " + (this.session != null && session.getId() != null));
6. }
7.
8. if (this.session == null
9.
10. //added in 1.2:
11. if
12. "Session creation has been disabled for the current subject. This exception indicates "
13. "that there is either a programming error (using a session when it should never be "
14. "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created "
15. "for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc "
16. "for more.";
17. throw new
18. }
19.
20. "Starting session for host {}", getHost());
21. SessionContext sessionContext = createSessionContext();
22. this.securityManager.start(sessionContext);
23. this.session = decorate(session);
24. }
25. return this.session;
26. }

getSession()的参数表示是否创建session,如果Session为空,并且传递的参数为true,则会创建一个Session。然而这里传递的是false,也就是说不会在创建Subject的时候来创建Session,所以把创建Session过程说完后,再回到此处是要记着不会去创建一个Session。但是我们可以来看下是如何创建Session的,整体三大步骤,先创建一个SessionContext ,然后根据SessionContext 来创建Session,最后是装饰Session,由于创建Session过程内容比较多,先说说装饰Session。

 

装饰Session就是讲Session和DelegatingSubject封装起来。 

1. protected
2. if (session == null) {
3. throw new IllegalArgumentException("session cannot be null");
4. }
5. return new StoppingAwareProxiedSession(session, this);
6. }

然后来说Session的创建过程,这和Subject的创建方式差不多。 

同样是SessionContext的接口设计:

 



shiro源码分析(二)Subject和Session_ci_03

 


和SubjectContext相当雷同。 

看下SessionContext的主要内容:

 


1. void
2. String getHost();
3.
4. Serializable getSessionId();
5.
6. void



主要两个内容,host和sessionId。 

接下来看下如何由SessionContext来创建Session:

 

1. protected
2. Session s = newSessionInstance(context);
3. if
4. "Creating session for host {}", s.getHost());
5. }
6. create(s);
7. return
8. }
9.
10. protected
11. return
12. }

和Subject一样也是由一个SessionFactory根据SessionContext来创建出一个Session,看下默认的SessionFactory SimpleSessionFactory的创建过程:

 


1. public
2. if (initData != null) {
3. String host = initData.getHost();
4. if (host != null) {
5. return new
6. }
7. }
8. return new
9. }



如果SessionContext有host信息,就传递给Session,然后就是直接new一个Session接口的实现SimpleSession,先看下Session接口有哪些内容:

 

1. public interface
2. Serializable getId();
3. Date getStartTimestamp();
4. Date getLastAccessTime();
5. long getTimeout() throws
6. void setTimeout(long maxIdleTimeInMillis) throws
7. String getHost();
8. void touch() throws
9. void stop() throws
10. throws
11. throws
12. void setAttribute(Object key, Object value) throws
13. throws
14. }



id:Session的唯一标识,创建时间、超时时间等内容。 

再看SimpleSession的创建过程:

 

1. public
2. this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT;
3. this.startTimestamp = new
4. this.lastAccessTime = this.startTimestamp;
5. }
6.
7. public
8. this();
9. this.host = host;
10. }



设置下超时时间为DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT 30分钟,startTimestamp 和lastAccessTime设置为现在开始。就这样构建出了一个Session的实例,然后就是需要将该实例保存起来:

 

1. protected
2. Session s = newSessionInstance(context);
3. if
4. "Creating session for host {}", s.getHost());
5. }
6. create(s);
7. return
8. }
9. protected void
10. if
11. "Creating new EIS record for new session instance [" + session + "]");
12. }
13. sessionDAO.create(session);
14. }



即该进行create(s)操作了,又和Subject极度的相像,使用sessionDAO来保存刚才创建的Session。再来看下SessionDAO接口:

 

1. public interface
2. Serializable create(Session session);
3. throws
4. void update(Session session) throws
5. void
6. Collection<Session> getActiveSessions();
7. }



也就是对所有的Session进行增删该查,SessionDAO 接口继承关系如下:

 



shiro源码分析(二)Subject和Session_Java_04

 


AbstractSessionDAO:有一个重要的属性SessionIdGenerator,它负责给Session创建sessionId,SessionIdGenerator接口如下:

 

1. public interface
2. Serializable generateId(Session session);
3. }



很简单,参数为Session,返回sessionId。SessionIdGenerator 的实现有两个JavaUuidSessionIdGenerator、RandomSessionIdGenerator。而AbstractSessionDAO默认采用的是JavaUuidSessionIdGenerator,如下:

 

1. public
2. this.sessionIdGenerator = new
3. }



MemorySessionDAO继承了AbstractSessionDAO,它把Session存储在一个ConcurrentMap<Serializable, Session> sessions集合中,key为sessionId,value为Session。 

CachingSessionDAO:主要配合在别的地方存储session。先不介绍,之后的文章再详细说。 

对于本案例来说SessionDAO为MemorySessionDAO。至此整个Session的创建过程就走通了。 


刚才虽然说了整个Session的创建过程,回到上文所说的,不会去创建Session的地方。在创建Subject搜集session信息时,使用的此时的Subject的Session、sessionId都为空,所以获取不到Session。然后就是doCreateSubject:

 

1. protected
2. return
3. }



就是通过SubjectFactory工厂接口来创建Subject的,而DefaultSecurityManager默认使用的 

SubjectFactory是DefaultSubjectFactory:

 

1. public
2. super();
3. this.subjectFactory = new
4. this.subjectDAO = new
5. }



继续看DefaultSubjectFactory是怎么创建Subject的:

 


1. public
2. SecurityManager securityManager = context.resolveSecurityManager();
3. Session session = context.resolveSession();
4. boolean
5. PrincipalCollection principals = context.resolvePrincipals();
6. boolean
7. String host = context.resolveHost();
8.
9. return new
10. }



仍然就是将这些属性传递给DelegatingSubject,也没什么好说的。创建完成之后,就需要将刚创建的Subject保存起来,仍回到:

 

1. public
2. //create a copy so we don't modify the argument's backing map:
3. SubjectContext context = copy(subjectContext);
4.
5. //ensure that the context has a SecurityManager instance, and if not, add one:
6. context = ensureSecurityManager(context);
7.
8. //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
9. //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
10. //process is often environment specific - better to shield the SF from these details:
11. context = resolveSession(context);
12.
13. //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
14. //if possible before handing off to the SubjectFactory:
15. context = resolvePrincipals(context);
16.
17. Subject subject = doCreateSubject(context);
18.
19. //save this subject for future reference if necessary:
20. //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
21. //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
22. //Added in 1.2:
23. save(subject);
24.
25. return
26. }



来看下save方法:

 


1. protected void
2. this.subjectDAO.save(subject);
3. }



可以看到又是使用另一个模块来完成的即SubjectDAO,SubjectDAO接口如下:

 

1. public interface
2. Subject save(Subject subject);
3. void
4. }



很简单,就是保存和删除一个Subject。我们看下具体的实现类DefaultSubjectDAO是如何来保存的:

 


1. public
2. if
3. saveToSession(subject);
4. else
5. "Session storage of subject state for Subject [{}] has been disabled: identity and "
6. "authentication state are expected to be initialized on every request or invocation.", subject);
7. }
8.
9. return
10. }



首先就是判断isSessionStorageEnabled,是否要存储该Subject的session来 

DefaultSubjectDAO:有一个重要属性SessionStorageEvaluator,它是用来决定一个Subject的Session来记录Subject的状态,接口如下

 

1. public interface
2. boolean
3. }

其实现为DefaultSessionStorageEvaluator:

 

1. public class DefaultSessionStorageEvaluator implements
2.
3. private boolean sessionStorageEnabled = true;
4.
5. public boolean
6. return (subject != null && subject.getSession(false) != null) || isSessionStorageEnabled();
7. }



决定策略就是通过DefaultSessionStorageEvaluator 的sessionStorageEnabled的true或false 和subject是否有Session对象来决定的。如果允许存储Subject的Session的话,下面就说具体的存储过程:

 

1. protected void
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
7. //merge PrincipalCollection state:
8.
9. null;
10.
11. //SHIRO-380: added if/else block - need to retain original (source) principals
12. //This technique (reflection) is only temporary - a proper long term solution needs to be found,
13. //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
14. //
15. //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
16. if (subject.isRunAs() && subject instanceof
17. try
18. class.getDeclaredField("principals");
19. true);
20. currentPrincipals = (PrincipalCollection)field.get(subject);
21. catch
22. throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
23. }
24. }
25. if (currentPrincipals == null
26. currentPrincipals = subject.getPrincipals();
27. }
28.
29. false);
30.
31. if (session == null) {
32. //只有当Session为空,并且currentPrincipals不为空的时候才会去创建Session
33. //Subject subject = SecurityUtils.getSubject()此时两者都是为空的,
34. //不会去创建Session
35. if
36. session = subject.getSession();
37. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
38. }
39. //otherwise no session and no principals - nothing to save
40. else
41. PrincipalCollection existingPrincipals =
42. (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
43.
44. if
45. if
46. session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
47. }
48. //otherwise both are null or empty - no need to update the session
49. else
50. if
51. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
52. }
53. //otherwise they're the same - no need to update the session
54. }
55. }
56. }



上面有我们关心的重点,当subject.getSession(false)获取的Session为空时(它不会去创建Session),此时就需要去创建Session,subject.getSession()则默认调用的是subject.getSession(true),则会进行Session的创建,创建过程上文已详细说明了。 

在第一次创建Subject的时候

1. Subject subject = SecurityUtils.getSubject();

虽然Session为空,但此时还没有用户身份信息,也不会去创建Session。案例中的subject.login(token),该过程则会去创建Session,具体看下过程:

 

1. public Subject login(Subject subject, AuthenticationToken token) throws
2. AuthenticationInfo info;
3. try
4. info = authenticate(token);
5. catch
6. try
7. onFailedLogin(token, ae, subject);
8. catch
9. if
10. "onFailedLogin method threw an "
11. "exception. Logging and propagating original AuthenticationException.", e);
12. }
13. }
14. throw ae; //propagate
15. }
16. //在该过程会进行Session的创建
17. Subject loggedIn = createSubject(token, info, subject);
18.
19. onSuccessfulLogin(token, info, loggedIn);
20.
21. return
22. }



对于验证过程上篇文章已经简单说明了,这里不再说明,重点还是在验证通过后,会设置Subject的身份,即用户名:

 

1. protected
2. SubjectContext context = createSubjectContext();
3. true);
4. context.setAuthenticationToken(token);
5. context.setAuthenticationInfo(info);
6. if (existing != null) {
7. context.setSubject(existing);
8. }
9. return
10. }



有了认证成功的AuthenticationInfo信息,SubjectContext在resolvePrincipals便可以获取用户信息,即通过AuthenticationInfo的getPrincipals()来获得。

 

1. public
2. PrincipalCollection principals = getPrincipals();
3.
4. if
5. //check to see if they were just authenticated:
6. AuthenticationInfo info = getAuthenticationInfo();
7. if (info != null) {
8. principals = info.getPrincipals();
9. }
10. }
11.
12. if
13. Subject subject = getSubject();
14. if (subject != null) {
15. principals = subject.getPrincipals();
16. }
17. }
18.
19. if
20. //try the session:
21. Session session = resolveSession();
22. if (session != null) {
23. principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
24. }
25. }
26.
27. return
28. }



PrincipalCollection不为空了,在save(subject)的时候会得到session为空,同时PrincipalCollection不为空,则会执行Session的创建。也就是说在认证通过后,会执行Session的创建,Session创建完成之后会进行一次装饰,即用StoppingAwareProxiedSession将创建出来的session和subject关联起来,然后又进行如下操作:

 

1. public void login(AuthenticationToken token) throws
2. clearRunAsIdentitiesInternal();
3. //这里的Subject则是经过认证后创建的并且也含有刚才创建的session,类型为
4. //StoppingAwareProxiedSession,即是该subject本身和session的合体。
5. this, token);
6.
7. PrincipalCollection principals;
8.
9. null;
10.
11. if (subject instanceof
12. DelegatingSubject delegating = (DelegatingSubject) subject;
13. //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
14. principals = delegating.principals;
15. host = delegating.host;
16. else
17. principals = subject.getPrincipals();
18. }
19.
20. if (principals == null
21. "Principals returned from securityManager.login( token ) returned a null or "
22. "empty value. This value must be non null and populated with one or more elements.";
23. throw new
24. }
25. this.principals = principals;
26. this.authenticated = true;
27. if (token instanceof
28. host = ((HostAuthenticationToken) token).getHost();
29. }
30. if (host != null) {
31. this.host = host;
32. }
33. false);
34. if (session != null) {
35. //在这里可以看到又进行了一次装饰
36. this.session = decorate(session);
37. else
38. this.session = null;
39. }
40. }

subject 创建出来之后,暂且叫内部subject,就是把认证通过的内部subject的信息和session复制给我们外界使用的subject.login(token)的subject中,这个subject暂且叫外部subject,看下session的赋值,又进行了一次装饰,这次装饰则把session(类型为StoppingAwareProxiedSession,即是内部subject和session的合体)和外部subject绑定到一起。 

最后来总结下,首先是Subject和Session的接口类图: 


shiro源码分析(二)Subject和Session_lua_05


然后就是Subject subject = SecurityUtils.getSubject()的一个简易的流程图: 

shiro源码分析(二)Subject和Session_Java_06



最后是subject.login(token)的简易流程图: 

shiro源码分析(二)Subject和Session_lua_07


shiro源码分析(二)Subject和Session_Java_08