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