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 */
019package org.apache.shiro.mgt;
020
021import org.apache.shiro.session.Session;
022import org.apache.shiro.subject.PrincipalCollection;
023import org.apache.shiro.subject.Subject;
024import org.apache.shiro.subject.support.DefaultSubjectContext;
025import org.apache.shiro.subject.support.DelegatingSubject;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import java.lang.reflect.Field;
030
031/**
032 * Default {@code SubjectDAO} implementation that stores Subject state in the Subject's Session by default (but this
033 * can be disabled - see below).  The Subject instance
034 * can be re-created at a later time by first acquiring the associated Session (typically from a
035 * {@link org.apache.shiro.session.mgt.SessionManager SessionManager}) via a session ID or session key and then
036 * building a {@code Subject} instance from {@code Session} attributes.
037 * <h2>Controlling how Sessions are used</h2>
038 * Whether or not a {@code Subject}'s {@code Session} is used or not to persist its own state is controlled on a
039 * <em>per-Subject</em> basis as determined by the configured
040 * {@link #setSessionStorageEvaluator(SessionStorageEvaluator) sessionStorageEvaluator}.
041 * The default {@code Evaluator} is a {@link DefaultSessionStorageEvaluator}, which supports enabling or disabling
042 * session usage for Subject persistence at a global level for all subjects (and defaults to allowing sessions to be
043 * used).
044 * <h3>Disabling Session Persistence Entirely</h3>
045 * Because the default {@code SessionStorageEvaluator} instance is a {@link DefaultSessionStorageEvaluator}, you
046 * can disable Session usage for Subject state entirely by configuring that instance directly, e.g.:
047 * <pre>
048 *     ((DefaultSessionStorageEvaluator)sessionDAO.getSessionStorageEvaluator()).setSessionStorageEnabled(false);
049 * </pre>
050 * or, for example, in {@code shiro.ini}:
051 * <pre>
052 *     securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
053 * </pre>
054 * but <b>note:</b> ONLY do this your
055 * application is 100% stateless and you <em>DO NOT</em> need subjects to be remembered across remote
056 * invocations, or in a web environment across HTTP requests.
057 * <h3>Supporting Both Stateful and Stateless Subject paradigms</h3>
058 * Perhaps your application needs to support a hybrid approach of both stateful and stateless Subjects:
059 * <ul>
060 * <li>Stateful: Stateful subjects might represent web end-users that need their identity and authentication
061 * state to be remembered from page to page.</li>
062 * <li>Stateless: Stateless subjects might represent API clients (e.g. REST clients) that authenticate on every
063 * request, and therefore don't need authentication state to be stored across requests in a session.</li>
064 * </ul>
065 * To support the hybrid <em>per-Subject</em> approach, you will need to create your own implementation of the
066 * {@link SessionStorageEvaluator} interface and configure it via the
067 * {@link #setSessionStorageEvaluator(SessionStorageEvaluator)} method, or, with {@code shiro.ini}:
068 * <pre>
069 *     myEvaluator = com.my.CustomSessionStorageEvaluator
070 *     securityManager.subjectDAO.sessionStorageEvaluator = $myEvaluator
071 * </pre>
072 * <p/>
073 * Unless overridden, the default evaluator is a {@link DefaultSessionStorageEvaluator}, which enables session usage for
074 * Subject state by default.
075 *
076 * @see #isSessionStorageEnabled(org.apache.shiro.subject.Subject)
077 * @see SessionStorageEvaluator
078 * @see DefaultSessionStorageEvaluator
079 * @since 1.2
080 */
081public class DefaultSubjectDAO implements SubjectDAO {
082
083    private static final Logger log = LoggerFactory.getLogger(DefaultSubjectDAO.class);
084
085    /**
086     * Evaluator that determines if a Subject's session may be used to store the Subject's own state.
087     */
088    private SessionStorageEvaluator sessionStorageEvaluator;
089
090    public DefaultSubjectDAO() {
091        //default implementation allows enabling/disabling session usages at a global level for all subjects:
092        this.sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
093    }
094
095    /**
096     * Determines if the subject's session will be used to persist subject state or not.  This implementation
097     * merely delegates to the internal {@link SessionStorageEvaluator} (a
098     * {@code DefaultSessionStorageEvaluator} by default).
099     *
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}