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}