001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.mgt;
020    
021    import org.apache.shiro.session.Session;
022    import org.apache.shiro.subject.PrincipalCollection;
023    import org.apache.shiro.subject.Subject;
024    import org.apache.shiro.subject.support.DefaultSubjectContext;
025    import org.apache.shiro.subject.support.DelegatingSubject;
026    import org.apache.shiro.util.CollectionUtils;
027    import org.slf4j.Logger;
028    import org.slf4j.LoggerFactory;
029    
030    import java.lang.reflect.Field;
031    
032    /**
033     * Default {@code SubjectDAO} implementation that stores Subject state in the Subject's Session by default (but this
034     * can be disabled - see below).  The Subject instance
035     * can be re-created at a later time by first acquiring the associated Session (typically from a
036     * {@link org.apache.shiro.session.mgt.SessionManager SessionManager}) via a session ID or session key and then
037     * building a {@code Subject} instance from {@code Session} attributes.
038     * <h2>Controlling how Sessions are used</h2>
039     * Whether or not a {@code Subject}'s {@code Session} is used or not to persist its own state is controlled on a
040     * <em>per-Subject</em> basis as determined by the configured
041     * {@link #setSessionStorageEvaluator(SessionStorageEvaluator) sessionStorageEvaluator}.
042     * The default {@code Evaluator} is a {@link DefaultSessionStorageEvaluator}, which supports enabling or disabling
043     * session usage for Subject persistence at a global level for all subjects (and defaults to allowing sessions to be
044     * used).
045     * <h3>Disabling Session Persistence Entirely</h3>
046     * Because the default {@code SessionStorageEvaluator} instance is a {@link DefaultSessionStorageEvaluator}, you
047     * can disable Session usage for Subject state entirely by configuring that instance directly, e.g.:
048     * <pre>
049     *     ((DefaultSessionStorageEvaluator)sessionDAO.getSessionStorageEvaluator()).setSessionStorageEnabled(false);
050     * </pre>
051     * or, for example, in {@code shiro.ini}:
052     * <pre>
053     *     securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
054     * </pre>
055     * but <b>note:</b> ONLY do this your
056     * application is 100% stateless and you <em>DO NOT</em> need subjects to be remembered across remote
057     * invocations, or in a web environment across HTTP requests.
058     * <h3>Supporting Both Stateful and Stateless Subject paradigms</h3>
059     * Perhaps your application needs to support a hybrid approach of both stateful and stateless Subjects:
060     * <ul>
061     * <li>Stateful: Stateful subjects might represent web end-users that need their identity and authentication
062     * state to be remembered from page to page.</li>
063     * <li>Stateless: Stateless subjects might represent API clients (e.g. REST clients) that authenticate on every
064     * request, and therefore don't need authentication state to be stored across requests in a session.</li>
065     * </ul>
066     * To support the hybrid <em>per-Subject</em> approach, you will need to create your own implementation of the
067     * {@link SessionStorageEvaluator} interface and configure it via the
068     * {@link #setSessionStorageEvaluator(SessionStorageEvaluator)} method, or, with {@code shiro.ini}:
069     * <pre>
070     *     myEvaluator = com.my.CustomSessionStorageEvaluator
071     *     securityManager.subjectDAO.sessionStorageEvaluator = $myEvaluator
072     * </pre>
073     * <p/>
074     * Unless overridden, the default evaluator is a {@link DefaultSessionStorageEvaluator}, which enables session usage for
075     * Subject state by default.
076     *
077     * @see #isSessionStorageEnabled(org.apache.shiro.subject.Subject)
078     * @see SessionStorageEvaluator
079     * @see DefaultSessionStorageEvaluator
080     * @since 1.2
081     */
082    public class DefaultSubjectDAO implements SubjectDAO {
083    
084        private static final Logger log = LoggerFactory.getLogger(DefaultSubjectDAO.class);
085    
086        /**
087         * Evaluator that determines if a Subject's session may be used to store the Subject's own state.
088         */
089        private SessionStorageEvaluator sessionStorageEvaluator;
090    
091        public DefaultSubjectDAO() {
092            //default implementation allows enabling/disabling session usages at a global level for all subjects:
093            this.sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
094        }
095    
096        /**
097         * Determines if the subject's session will be used to persist subject state or not.  This implementation
098         * merely delegates to the internal {@link SessionStorageEvaluator} (a
099         * {@code DefaultSessionStorageEvaluator} by default).
100         *
101         * @param subject the subject to inspect to determine if the subject's session will be used to persist subject
102         *                state or not.
103         * @return {@code true} if the subject's session will be used to persist subject state, {@code false} otherwise.
104         * @see #setSessionStorageEvaluator(SessionStorageEvaluator)
105         * @see DefaultSessionStorageEvaluator
106         */
107        protected boolean isSessionStorageEnabled(Subject subject) {
108            return getSessionStorageEvaluator().isSessionStorageEnabled(subject);
109        }
110    
111        /**
112         * Returns the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
113         * the Subject's session.  The default instance is a {@link DefaultSessionStorageEvaluator}.
114         *
115         * @return the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
116         *         the Subject's session.
117         * @see DefaultSessionStorageEvaluator
118         */
119        public SessionStorageEvaluator getSessionStorageEvaluator() {
120            return sessionStorageEvaluator;
121        }
122    
123        /**
124         * Sets the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
125         * the Subject's session. The default instance is a {@link DefaultSessionStorageEvaluator}.
126         *
127         * @param sessionStorageEvaluator the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s
128         *                                state may be persisted in the Subject's session.
129         * @see DefaultSessionStorageEvaluator
130         */
131        public void setSessionStorageEvaluator(SessionStorageEvaluator sessionStorageEvaluator) {
132            this.sessionStorageEvaluator = sessionStorageEvaluator;
133        }
134    
135        /**
136         * Saves the subject's state to the subject's {@link org.apache.shiro.subject.Subject#getSession() session} only
137         * if {@link #isSessionStorageEnabled(Subject) sessionStorageEnabled(subject)}.  If session storage is not enabled
138         * for the specific {@code Subject}, this method does nothing.
139         * <p/>
140         * In either case, the argument {@code Subject} is returned directly (a new Subject instance is not created).
141         *
142         * @param subject the Subject instance for which its state will be created or updated.
143         * @return the same {@code Subject} passed in (a new Subject instance is not created).
144         */
145        public Subject save(Subject subject) {
146            if (isSessionStorageEnabled(subject)) {
147                saveToSession(subject);
148            } else {
149                log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
150                        "authentication state are expected to be initialized on every request or invocation.", subject);
151            }
152    
153            return subject;
154        }
155    
156        /**
157         * Saves the subject's state (it's principals and authentication state) to its
158         * {@link org.apache.shiro.subject.Subject#getSession() session}.  The session can be retrieved at a later time
159         * (typically from a {@link org.apache.shiro.session.mgt.SessionManager SessionManager} to be used to recreate
160         * the {@code Subject} instance.
161         *
162         * @param subject the subject for which state will be persisted to its session.
163         */
164        protected void saveToSession(Subject subject) {
165            //performs merge logic, only updating the Subject's session if it does not match the current state:
166            mergePrincipals(subject);
167            mergeAuthenticationState(subject);
168        }
169    
170        /**
171         * Merges the Subject's current {@link org.apache.shiro.subject.Subject#getPrincipals()} with whatever may be in
172         * any available session.  Only updates the Subject's session if the session does not match the current principals
173         * state.
174         *
175         * @param subject the Subject for which principals will potentially be merged into the Subject's session.
176         */
177        protected void mergePrincipals(Subject subject) {
178            //merge PrincipalCollection state:
179    
180            PrincipalCollection currentPrincipals = null;
181    
182            //SHIRO-380: added if/else block - need to retain original (source) principals
183            //This technique (reflection) is only temporary - a proper long term solution needs to be found,
184            //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
185            //
186            //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
187            if (subject.isRunAs() && subject instanceof DelegatingSubject) {
188                try {
189                    Field field = DelegatingSubject.class.getDeclaredField("principals");
190                    field.setAccessible(true);
191                    currentPrincipals = (PrincipalCollection)field.get(subject);
192                } catch (Exception e) {
193                    throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
194                }
195            }
196            if (currentPrincipals == null || currentPrincipals.isEmpty()) {
197                currentPrincipals = subject.getPrincipals();
198            }
199    
200            Session session = subject.getSession(false);
201    
202            if (session == null) {
203                if (!CollectionUtils.isEmpty(currentPrincipals)) {
204                    session = subject.getSession();
205                    session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
206                }
207                //otherwise no session and no principals - nothing to save
208            } else {
209                PrincipalCollection existingPrincipals =
210                        (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
211    
212                if (CollectionUtils.isEmpty(currentPrincipals)) {
213                    if (!CollectionUtils.isEmpty(existingPrincipals)) {
214                        session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
215                    }
216                    //otherwise both are null or empty - no need to update the session
217                } else {
218                    if (!currentPrincipals.equals(existingPrincipals)) {
219                        session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
220                    }
221                    //otherwise they're the same - no need to update the session
222                }
223            }
224        }
225    
226        /**
227         * Merges the Subject's current authentication state with whatever may be in
228         * any available session.  Only updates the Subject's session if the session does not match the current
229         * authentication state.
230         *
231         * @param subject the Subject for which principals will potentially be merged into the Subject's session.
232         */
233        protected void mergeAuthenticationState(Subject subject) {
234    
235            Session session = subject.getSession(false);
236    
237            if (session == null) {
238                if (subject.isAuthenticated()) {
239                    session = subject.getSession();
240                    session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
241                }
242                //otherwise no session and not authenticated - nothing to save
243            } else {
244                Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
245    
246                if (subject.isAuthenticated()) {
247                    if (existingAuthc == null || !existingAuthc) {
248                        session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
249                    }
250                    //otherwise authc state matches - no need to update the session
251                } else {
252                    if (existingAuthc != null) {
253                        //existing doesn't match the current state - remove it:
254                        session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
255                    }
256                    //otherwise not in the session and not authenticated - no need to update the session
257                }
258            }
259        }
260    
261        /**
262         * Removes any existing subject state from the Subject's session (if the session exists).  If the session
263         * does not exist, this method does not do anything.
264         *
265         * @param subject the subject for which any existing subject state will be removed from its session.
266         */
267        protected void removeFromSession(Subject subject) {
268            Session session = subject.getSession(false);
269            if (session != null) {
270                session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
271                session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
272            }
273        }
274    
275        /**
276         * Removes any existing subject state from the subject's session (if the session exists).
277         *
278         * @param subject the Subject instance for which any persistent state should be deleted.
279         */
280        public void delete(Subject subject) {
281            removeFromSession(subject);
282        }
283    }