Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DefaultSubjectDAO |
|
| 2.9;2.9 |
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.apache.shiro.util.CollectionUtils; | |
27 | import org.slf4j.Logger; | |
28 | import org.slf4j.LoggerFactory; | |
29 | ||
30 | import java.lang.reflect.Field; | |
31 | ||
32 | /** | |
33 | * Default {@code SubjectDAO} implementation that stores Subject state in the Subject's Session by default (but this | |
34 | * can be disabled - see below). The Subject instance | |
35 | * can be re-created at a later time by first acquiring the associated Session (typically from a | |
36 | * {@link org.apache.shiro.session.mgt.SessionManager SessionManager}) via a session ID or session key and then | |
37 | * building a {@code Subject} instance from {@code Session} attributes. | |
38 | * <h2>Controlling how Sessions are used</h2> | |
39 | * Whether or not a {@code Subject}'s {@code Session} is used or not to persist its own state is controlled on a | |
40 | * <em>per-Subject</em> basis as determined by the configured | |
41 | * {@link #setSessionStorageEvaluator(SessionStorageEvaluator) sessionStorageEvaluator}. | |
42 | * The default {@code Evaluator} is a {@link DefaultSessionStorageEvaluator}, which supports enabling or disabling | |
43 | * session usage for Subject persistence at a global level for all subjects (and defaults to allowing sessions to be | |
44 | * used). | |
45 | * <h3>Disabling Session Persistence Entirely</h3> | |
46 | * Because the default {@code SessionStorageEvaluator} instance is a {@link DefaultSessionStorageEvaluator}, you | |
47 | * can disable Session usage for Subject state entirely by configuring that instance directly, e.g.: | |
48 | * <pre> | |
49 | * ((DefaultSessionStorageEvaluator)sessionDAO.getSessionStorageEvaluator()).setSessionStorageEnabled(false); | |
50 | * </pre> | |
51 | * or, for example, in {@code shiro.ini}: | |
52 | * <pre> | |
53 | * securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false | |
54 | * </pre> | |
55 | * but <b>note:</b> ONLY do this your | |
56 | * application is 100% stateless and you <em>DO NOT</em> need subjects to be remembered across remote | |
57 | * invocations, or in a web environment across HTTP requests. | |
58 | * <h3>Supporting Both Stateful and Stateless Subject paradigms</h3> | |
59 | * Perhaps your application needs to support a hybrid approach of both stateful and stateless Subjects: | |
60 | * <ul> | |
61 | * <li>Stateful: Stateful subjects might represent web end-users that need their identity and authentication | |
62 | * state to be remembered from page to page.</li> | |
63 | * <li>Stateless: Stateless subjects might represent API clients (e.g. REST clients) that authenticate on every | |
64 | * request, and therefore don't need authentication state to be stored across requests in a session.</li> | |
65 | * </ul> | |
66 | * To support the hybrid <em>per-Subject</em> approach, you will need to create your own implementation of the | |
67 | * {@link SessionStorageEvaluator} interface and configure it via the | |
68 | * {@link #setSessionStorageEvaluator(SessionStorageEvaluator)} method, or, with {@code shiro.ini}: | |
69 | * <pre> | |
70 | * myEvaluator = com.my.CustomSessionStorageEvaluator | |
71 | * securityManager.subjectDAO.sessionStorageEvaluator = $myEvaluator | |
72 | * </pre> | |
73 | * <p/> | |
74 | * Unless overridden, the default evaluator is a {@link DefaultSessionStorageEvaluator}, which enables session usage for | |
75 | * Subject state by default. | |
76 | * | |
77 | * @see #isSessionStorageEnabled(org.apache.shiro.subject.Subject) | |
78 | * @see SessionStorageEvaluator | |
79 | * @see DefaultSessionStorageEvaluator | |
80 | * @since 1.2 | |
81 | */ | |
82 | public class DefaultSubjectDAO implements SubjectDAO { | |
83 | ||
84 | 1 | private static final Logger log = LoggerFactory.getLogger(DefaultSubjectDAO.class); |
85 | ||
86 | /** | |
87 | * Evaluator that determines if a Subject's session may be used to store the Subject's own state. | |
88 | */ | |
89 | private SessionStorageEvaluator sessionStorageEvaluator; | |
90 | ||
91 | 54 | public DefaultSubjectDAO() { |
92 | //default implementation allows enabling/disabling session usages at a global level for all subjects: | |
93 | 54 | this.sessionStorageEvaluator = new DefaultSessionStorageEvaluator(); |
94 | 54 | } |
95 | ||
96 | /** | |
97 | * Determines if the subject's session will be used to persist subject state or not. This implementation | |
98 | * merely delegates to the internal {@link SessionStorageEvaluator} (a | |
99 | * {@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 | 51 | 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 | 53 | 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 | 1 | this.sessionStorageEvaluator = sessionStorageEvaluator; |
133 | 1 | } |
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 | 48 | if (isSessionStorageEnabled(subject)) { |
147 | 47 | saveToSession(subject); |
148 | } else { | |
149 | 1 | 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 | 48 | 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 | 47 | mergePrincipals(subject); |
167 | 47 | mergeAuthenticationState(subject); |
168 | 47 | } |
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 | 53 | 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 | 53 | if (subject.isRunAs() && subject instanceof DelegatingSubject) { |
188 | try { | |
189 | 1 | Field field = DelegatingSubject.class.getDeclaredField("principals"); |
190 | 1 | field.setAccessible(true); |
191 | 1 | currentPrincipals = (PrincipalCollection)field.get(subject); |
192 | 0 | } catch (Exception e) { |
193 | 0 | throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e); |
194 | 1 | } |
195 | } | |
196 | 53 | if (currentPrincipals == null || currentPrincipals.isEmpty()) { |
197 | 52 | currentPrincipals = subject.getPrincipals(); |
198 | } | |
199 | ||
200 | 53 | Session session = subject.getSession(false); |
201 | ||
202 | 53 | if (session == null) { |
203 | 48 | if (!CollectionUtils.isEmpty(currentPrincipals)) { |
204 | 18 | session = subject.getSession(); |
205 | 18 | session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); |
206 | } | |
207 | //otherwise no session and no principals - nothing to save | |
208 | } else { | |
209 | 5 | PrincipalCollection existingPrincipals = |
210 | (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); | |
211 | ||
212 | 5 | if (CollectionUtils.isEmpty(currentPrincipals)) { |
213 | 2 | if (!CollectionUtils.isEmpty(existingPrincipals)) { |
214 | 1 | session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); |
215 | } | |
216 | //otherwise both are null or empty - no need to update the session | |
217 | } else { | |
218 | 3 | if (!currentPrincipals.equals(existingPrincipals)) { |
219 | 3 | session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); |
220 | } | |
221 | //otherwise they're the same - no need to update the session | |
222 | } | |
223 | } | |
224 | 53 | } |
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 | 52 | Session session = subject.getSession(false); |
236 | ||
237 | 52 | if (session == null) { |
238 | 31 | if (subject.isAuthenticated()) { |
239 | 1 | session = subject.getSession(); |
240 | 1 | session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE); |
241 | } | |
242 | //otherwise no session and not authenticated - nothing to save | |
243 | } else { | |
244 | 21 | Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY); |
245 | ||
246 | 21 | if (subject.isAuthenticated()) { |
247 | 19 | if (existingAuthc == null || !existingAuthc) { |
248 | 19 | session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE); |
249 | } | |
250 | //otherwise authc state matches - no need to update the session | |
251 | } else { | |
252 | 2 | if (existingAuthc != null) { |
253 | //existing doesn't match the current state - remove it: | |
254 | 1 | 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 | 52 | } |
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 | 11 | Session session = subject.getSession(false); |
269 | 11 | if (session != null) { |
270 | 10 | session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY); |
271 | 10 | session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); |
272 | } | |
273 | 11 | } |
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 | 11 | removeFromSession(subject); |
282 | 11 | } |
283 | } |