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.web.mgt;
20  
21  import org.apache.shiro.codec.Base64;
22  import org.apache.shiro.mgt.AbstractRememberMeManager;
23  import org.apache.shiro.subject.Subject;
24  import org.apache.shiro.subject.SubjectContext;
25  import org.apache.shiro.web.servlet.Cookie;
26  import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
27  import org.apache.shiro.web.servlet.SimpleCookie;
28  import org.apache.shiro.web.subject.WebSubject;
29  import org.apache.shiro.web.subject.WebSubjectContext;
30  import org.apache.shiro.web.util.WebUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import javax.servlet.ServletRequest;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  
38  
39  /**
40   * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
41   * for later retrieval.
42   * <p/>
43   * Cookie attributes (path, domain, maxAge, etc) may be set on this class's default
44   * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies
45   * created by this implementation.
46   * <p/>
47   * The default cookie has the following attribute values set:
48   * <table>
49   * <tr>
50   * <th>Attribute Name</th>
51   * <th>Value</th>
52   * </tr>
53   * <tr><td>{@link Cookie#getName() name}</td>
54   * <td>{@code rememberMe}</td>
55   * </tr>
56   * <tr>
57   * <td>{@link Cookie#getPath() path}</td>
58   * <td>{@code /}</td>
59   * </tr>
60   * <tr>
61   * <td>{@link Cookie#getMaxAge() maxAge}</td>
62   * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td>
63   * </tr>
64   * </table>
65   * <p/>
66   * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization
67   * and encryption logic, this class utilizes both for added security before setting the cookie value.
68   *
69   * @since 1.0
70   */
71  public class CookieRememberMeManager extends AbstractRememberMeManager {
72  
73      //TODO - complete JavaDoc
74  
75      private static transient final Logger log = LoggerFactory.getLogger(CookieRememberMeManager.class);
76  
77      /**
78       * The default name of the underlying rememberMe cookie which is {@code rememberMe}.
79       */
80      public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe";
81  
82      private Cookie cookie;
83  
84      /**
85       * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template.
86       */
87      public CookieRememberMeManager() {
88          Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
89          cookie.setHttpOnly(true);
90          //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited
91          //in a year:
92          cookie.setMaxAge(Cookie.ONE_YEAR);
93          this.cookie = cookie;
94      }
95  
96      /**
97       * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
98       * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
99       * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
100      * <p/>
101      * Please see the class-level JavaDoc for the default cookie's attribute values.
102      *
103      * @return the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
104      *         this {@code RememberMeManager}.
105      */
106     public Cookie getCookie() {
107         return cookie;
108     }
109 
110     /**
111      * Sets the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
112      * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
113      * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
114      * <p/>
115      * Please see the class-level JavaDoc for the default cookie's attribute values.
116      *
117      * @param cookie the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created
118      *               by this {@code RememberMeManager}.
119      */
120     @SuppressWarnings({"UnusedDeclaration"})
121     public void setCookie(Cookie cookie) {
122         this.cookie = cookie;
123     }
124 
125     /**
126      * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
127      * <p/>
128      * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair
129      * so an HTTP cookie can be set on the outgoing response.  If it is not a {@code WebSubject} or that
130      * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing.
131      *
132      * @param subject    the Subject for which the identity is being serialized.
133      * @param serialized the serialized bytes to be persisted.
134      */
135     protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
136 
137         if (!WebUtils.isHttp(subject)) {
138             if (log.isDebugEnabled()) {
139                 String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
140                         "request and response in order to set the rememberMe cookie. Returning immediately and " +
141                         "ignoring rememberMe operation.";
142                 log.debug(msg);
143             }
144             return;
145         }
146 
147 
148         HttpServletRequest request = WebUtils.getHttpRequest(subject);
149         HttpServletResponse response = WebUtils.getHttpResponse(subject);
150 
151         //base 64 encode it and store as a cookie:
152         String base64 = Base64.encodeToString(serialized);
153 
154         Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
155         Cookie cookie = new SimpleCookie(template);
156         cookie.setValue(base64);
157         cookie.saveTo(request, response);
158     }
159 
160     private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
161         ServletRequest request = subjectContext.resolveServletRequest();
162         if (request != null) {
163             Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
164             return removed != null && removed;
165         }
166         return false;
167     }
168 
169 
170     /**
171      * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
172      * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte
173      * array.
174      * <p/>
175      * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
176      * Request/Response pair so an HTTP cookie can be retrieved from the incoming request.  If it is not a
177      * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this
178      * implementation returns {@code null}.
179      *
180      * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
181      *                       is being used to construct a {@link Subject} instance.  To be used to assist with data
182      *                       lookup.
183      * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
184      */
185     protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
186 
187         if (!WebUtils.isHttp(subjectContext)) {
188             if (log.isDebugEnabled()) {
189                 String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +
190                         "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
191                         "immediately and ignoring rememberMe operation.";
192                 log.debug(msg);
193             }
194             return null;
195         }
196 
197         WebSubjectContext/../../org/apache/shiro/web/subject/WebSubjectContext.html#WebSubjectContext">WebSubjectContext wsc = (WebSubjectContext) subjectContext;
198         if (isIdentityRemoved(wsc)) {
199             return null;
200         }
201 
202         HttpServletRequest request = WebUtils.getHttpRequest(wsc);
203         HttpServletResponse response = WebUtils.getHttpResponse(wsc);
204 
205         String base64 = getCookie().readValue(request, response);
206         // Browsers do not always remove cookies immediately (SHIRO-183)
207         // ignore cookies that are scheduled for removal
208         if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
209 
210         if (base64 != null) {
211             base64 = ensurePadding(base64);
212             if (log.isTraceEnabled()) {
213                 log.trace("Acquired Base64 encoded identity [" + base64 + "]");
214             }
215             byte[] decoded;
216             try {
217                 decoded = Base64.decode(base64);
218             } catch (RuntimeException rtEx) {
219                 /*
220                  * https://issues.apache.org/jira/browse/SHIRO-766:
221                  * If the base64 string cannot be decoded, just assume there is no valid cookie value.
222                  * */
223                 getCookie().removeFrom(request, response);
224                 log.warn("Unable to decode existing base64 encoded entity: [" + base64 + "].", rtEx);
225                 return null;
226             }
227 
228             if (log.isTraceEnabled()) {
229                 log.trace("Base64 decoded byte array length: " + decoded.length + " bytes.");
230             }
231             return decoded;
232         } else {
233             //no cookie set - new site visitor?
234             return null;
235         }
236     }
237 
238     /**
239      * Sometimes a user agent will send the rememberMe cookie value without padding,
240      * most likely because {@code =} is a separator in the cookie header.
241      * <p/>
242      * Contributed by Luis Arias.  Thanks Luis!
243      *
244      * @param base64 the base64 encoded String that may need to be padded
245      * @return the base64 String padded if necessary.
246      */
247     private String ensurePadding(String base64) {
248         int length = base64.length();
249         if (length % 4 != 0) {
250             StringBuilder sb = new StringBuilder(base64);
251             for (int i = 0; i < length % 4; ++i) {
252                 sb.append('=');
253             }
254             base64 = sb.toString();
255         }
256         return base64;
257     }
258 
259     /**
260      * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair.
261      * <p/>
262      * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair.
263      * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this
264      * implementation does nothing.
265      *
266      * @param subject the subject instance for which identity data should be forgotten from the underlying persistence
267      */
268     protected void forgetIdentity(Subject subject) {
269         if (WebUtils.isHttp(subject)) {
270             HttpServletRequest request = WebUtils.getHttpRequest(subject);
271             HttpServletResponse response = WebUtils.getHttpResponse(subject);
272             forgetIdentity(request, response);
273         }
274     }
275 
276     /**
277      * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
278      * <p/>
279      * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
280      * Request/Response pair.  If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
281      * have an HTTP Request/Response pair, this implementation does nothing.
282      *
283      * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
284      */
285     public void forgetIdentity(SubjectContext subjectContext) {
286         if (WebUtils.isHttp(subjectContext)) {
287             HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
288             HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
289             forgetIdentity(request, response);
290         }
291     }
292 
293     /**
294      * Removes the rememberMe cookie from the given request/response pair.
295      *
296      * @param request  the incoming HTTP servlet request
297      * @param response the outgoing HTTP servlet response
298      */
299     private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
300         getCookie().removeFrom(request, response);
301     }
302 }
303