View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.mgt;
20  
21  import org.apache.shiro.session.Session;
22  import org.apache.shiro.subject.PrincipalCollection;
23  import org.apache.shiro.subject.Subject;
24  import org.apache.shiro.subject.support.DefaultSubjectContext;
25  import org.apache.shiro.subject.support.DelegatingSubject;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import java.lang.reflect.Field;
30  
31  /**
32   * Default {@code SubjectDAO} implementation that stores Subject state in the Subject's Session by default (but this
33   * can be disabled - see below).  The Subject instance
34   * can be re-created at a later time by first acquiring the associated Session (typically from a
35   * {@link org.apache.shiro.session.mgt.SessionManager SessionManager}) via a session ID or session key and then
36   * building a {@code Subject} instance from {@code Session} attributes.
37   * <h2>Controlling how Sessions are used</h2>
38   * Whether or not a {@code Subject}'s {@code Session} is used or not to persist its own state is controlled on a
39   * <em>per-Subject</em> basis as determined by the configured
40   * {@link #setSessionStorageEvaluator(SessionStorageEvaluator) sessionStorageEvaluator}.
41   * The default {@code Evaluator} is a {@link DefaultSessionStorageEvaluator}, which supports enabling or disabling
42   * session usage for Subject persistence at a global level for all subjects (and defaults to allowing sessions to be
43   * used).
44   * <h3>Disabling Session Persistence Entirely</h3>
45   * Because the default {@code SessionStorageEvaluator} instance is a {@link DefaultSessionStorageEvaluator}, you
46   * can disable Session usage for Subject state entirely by configuring that instance directly, e.g.:
47   * <pre>
48   *     ((DefaultSessionStorageEvaluator)sessionDAO.getSessionStorageEvaluator()).setSessionStorageEnabled(false);
49   * </pre>
50   * or, for example, in {@code shiro.ini}:
51   * <pre>
52   *     securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
53   * </pre>
54   * but <b>note:</b> ONLY do this your
55   * application is 100% stateless and you <em>DO NOT</em> need subjects to be remembered across remote
56   * invocations, or in a web environment across HTTP requests.
57   * <h3>Supporting Both Stateful and Stateless Subject paradigms</h3>
58   * Perhaps your application needs to support a hybrid approach of both stateful and stateless Subjects:
59   * <ul>
60   * <li>Stateful: Stateful subjects might represent web end-users that need their identity and authentication
61   * state to be remembered from page to page.</li>
62   * <li>Stateless: Stateless subjects might represent API clients (e.g. REST clients) that authenticate on every
63   * request, and therefore don't need authentication state to be stored across requests in a session.</li>
64   * </ul>
65   * To support the hybrid <em>per-Subject</em> approach, you will need to create your own implementation of the
66   * {@link SessionStorageEvaluator} interface and configure it via the
67   * {@link #setSessionStorageEvaluator(SessionStorageEvaluator)} method, or, with {@code shiro.ini}:
68   * <pre>
69   *     myEvaluator = com.my.CustomSessionStorageEvaluator
70   *     securityManager.subjectDAO.sessionStorageEvaluator = $myEvaluator
71   * </pre>
72   * <p/>
73   * Unless overridden, the default evaluator is a {@link DefaultSessionStorageEvaluator}, which enables session usage for
74   * Subject state by default.
75   *
76   * @see #isSessionStorageEnabled(org.apache.shiro.subject.Subject)
77   * @see SessionStorageEvaluator
78   * @see DefaultSessionStorageEvaluator
79   * @since 1.2
80   */
81  public class DefaultSubjectDAO implements SubjectDAO {
82  
83      private static final Logger log = LoggerFactory.getLogger(DefaultSubjectDAO.class);
84  
85      /**
86       * Evaluator that determines if a Subject's session may be used to store the Subject's own state.
87       */
88      private SessionStorageEvaluator sessionStorageEvaluator;
89  
90      public DefaultSubjectDAO() {
91          //default implementation allows enabling/disabling session usages at a global level for all subjects:
92          this.sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
93      }
94  
95      /**
96       * Determines if the subject's session will be used to persist subject state or not.  This implementation
97       * merely delegates to the internal {@link SessionStorageEvaluator} (a
98       * {@code DefaultSessionStorageEvaluator} by default).
99       *
100      * @param subject the subject to inspect to determine if the subject's session will be used to persist subject
101      *                state or not.
102      * @return {@code true} if the subject's session will be used to persist subject state, {@code false} otherwise.
103      * @see #setSessionStorageEvaluator(SessionStorageEvaluator)
104      * @see DefaultSessionStorageEvaluator
105      */
106     protected boolean isSessionStorageEnabled(Subject subject) {
107         return getSessionStorageEvaluator().isSessionStorageEnabled(subject);
108     }
109 
110     /**
111      * Returns the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
112      * the Subject's session.  The default instance is a {@link DefaultSessionStorageEvaluator}.
113      *
114      * @return the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
115      *         the Subject's session.
116      * @see DefaultSessionStorageEvaluator
117      */
118     public SessionStorageEvaluator getSessionStorageEvaluator() {
119         return sessionStorageEvaluator;
120     }
121 
122     /**
123      * Sets the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
124      * the Subject's session. The default instance is a {@link DefaultSessionStorageEvaluator}.
125      *
126      * @param sessionStorageEvaluator the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s
127      *                                state may be persisted in the Subject's session.
128      * @see DefaultSessionStorageEvaluator
129      */
130     public void setSessionStorageEvaluator(SessionStorageEvaluator sessionStorageEvaluator) {
131         this.sessionStorageEvaluator = sessionStorageEvaluator;
132     }
133 
134     /**
135      * Saves the subject's state to the subject's {@link org.apache.shiro.subject.Subject#getSession() session} only
136      * if {@link #isSessionStorageEnabled(Subject) sessionStorageEnabled(subject)}.  If session storage is not enabled
137      * for the specific {@code Subject}, this method does nothing.
138      * <p/>
139      * In either case, the argument {@code Subject} is returned directly (a new Subject instance is not created).
140      *
141      * @param subject the Subject instance for which its state will be created or updated.
142      * @return the same {@code Subject} passed in (a new Subject instance is not created).
143      */
144     public Subject save(Subject subject) {
145         if (isSessionStorageEnabled(subject)) {
146             saveToSession(subject);
147         } else {
148             log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
149                     "authentication state are expected to be initialized on every request or invocation.", subject);
150         }
151 
152         return subject;
153     }
154 
155     /**
156      * Saves the subject's state (it's principals and authentication state) to its
157      * {@link org.apache.shiro.subject.Subject#getSession() session}.  The session can be retrieved at a later time
158      * (typically from a {@link org.apache.shiro.session.mgt.SessionManager SessionManager} to be used to recreate
159      * the {@code Subject} instance.
160      *
161      * @param subject the subject for which state will be persisted to its session.
162      */
163     protected void saveToSession(Subject subject) {
164         //performs merge logic, only updating the Subject's session if it does not match the current state:
165         mergePrincipals(subject);
166         mergeAuthenticationState(subject);
167     }
168 
169     private static boolean isEmpty(PrincipalCollection pc) {
170         return pc == null || pc.isEmpty();
171     }
172 
173     /**
174      * Merges the Subject's current {@link org.apache.shiro.subject.Subject#getPrincipals()} with whatever may be in
175      * any available session.  Only updates the Subject's session if the session does not match the current principals
176      * state.
177      *
178      * @param subject the Subject for which principals will potentially be merged into the Subject's session.
179      */
180     protected void mergePrincipals(Subject subject) {
181         //merge PrincipalCollection state:
182 
183         PrincipalCollection currentPrincipals = null;
184 
185         //SHIRO-380: added if/else block - need to retain original (source) principals
186         //This technique (reflection) is only temporary - a proper long term solution needs to be found,
187         //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
188         //
189         //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
190         if (subject.isRunAs() && subject instanceof DelegatingSubject) {
191             try {
192                 Field field = DelegatingSubject.class.getDeclaredField("principals");
193                 field.setAccessible(true);
194                 currentPrincipals = (PrincipalCollection)field.get(subject);
195             } catch (Exception e) {
196                 throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
197             }
198         }
199         if (currentPrincipals == null || currentPrincipals.isEmpty()) {
200             currentPrincipals = subject.getPrincipals();
201         }
202 
203         Session session = subject.getSession(false);
204 
205         if (session == null) {
206             if (!isEmpty(currentPrincipals)) {
207                 session = subject.getSession();
208                 session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
209             }
210             // otherwise no session and no principals - nothing to save
211         } else {
212             PrincipalCollection existingPrincipals =
213                     (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
214 
215             if (isEmpty(currentPrincipals)) {
216                 if (!isEmpty(existingPrincipals)) {
217                     session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
218                 }
219                 // otherwise both are null or empty - no need to update the session
220             } else {
221                 if (!currentPrincipals.equals(existingPrincipals)) {
222                     session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
223                 }
224                 // otherwise they're the same - no need to update the session
225             }
226         }
227     }
228 
229     /**
230      * Merges the Subject's current authentication state with whatever may be in
231      * any available session.  Only updates the Subject's session if the session does not match the current
232      * authentication state.
233      *
234      * @param subject the Subject for which principals will potentially be merged into the Subject's session.
235      */
236     protected void mergeAuthenticationState(Subject subject) {
237 
238         Session session = subject.getSession(false);
239 
240         if (session == null) {
241             if (subject.isAuthenticated()) {
242                 session = subject.getSession();
243                 session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
244             }
245             //otherwise no session and not authenticated - nothing to save
246         } else {
247             Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
248 
249             if (subject.isAuthenticated()) {
250                 if (existingAuthc == null || !existingAuthc) {
251                     session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
252                 }
253                 //otherwise authc state matches - no need to update the session
254             } else {
255                 if (existingAuthc != null) {
256                     //existing doesn't match the current state - remove it:
257                     session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
258                 }
259                 //otherwise not in the session and not authenticated - no need to update the session
260             }
261         }
262     }
263 
264     /**
265      * Removes any existing subject state from the Subject's session (if the session exists).  If the session
266      * does not exist, this method does not do anything.
267      *
268      * @param subject the subject for which any existing subject state will be removed from its session.
269      */
270     protected void removeFromSession(Subject subject) {
271         Session session = subject.getSession(false);
272         if (session != null) {
273             session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
274             session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
275         }
276     }
277 
278     /**
279      * Removes any existing subject state from the subject's session (if the session exists).
280      *
281      * @param subject the Subject instance for which any persistent state should be deleted.
282      */
283     public void delete(Subject subject) {
284         removeFromSession(subject);
285     }
286 }