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 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 = Base64.decode(base64);
216             if (log.isTraceEnabled()) {
217                 log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
218             }
219             return decoded;
220         } else {
221             //no cookie set - new site visitor?
222             return null;
223         }
224     }
225 
226     /**
227      * Sometimes a user agent will send the rememberMe cookie value without padding,
228      * most likely because {@code =} is a separator in the cookie header.
229      * <p/>
230      * Contributed by Luis Arias.  Thanks Luis!
231      *
232      * @param base64 the base64 encoded String that may need to be padded
233      * @return the base64 String padded if necessary.
234      */
235     private String ensurePadding(String base64) {
236         int length = base64.length();
237         if (length % 4 != 0) {
238             StringBuilder sb = new StringBuilder(base64);
239             for (int i = 0; i < length % 4; ++i) {
240                 sb.append('=');
241             }
242             base64 = sb.toString();
243         }
244         return base64;
245     }
246 
247     /**
248      * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair.
249      * <p/>
250      * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair.
251      * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this
252      * implementation does nothing.
253      *
254      * @param subject the subject instance for which identity data should be forgotten from the underlying persistence
255      */
256     protected void forgetIdentity(Subject subject) {
257         if (WebUtils.isHttp(subject)) {
258             HttpServletRequest request = WebUtils.getHttpRequest(subject);
259             HttpServletResponse response = WebUtils.getHttpResponse(subject);
260             forgetIdentity(request, response);
261         }
262     }
263 
264     /**
265      * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
266      * <p/>
267      * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
268      * Request/Response pair.  If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
269      * have an HTTP Request/Response pair, this implementation does nothing.
270      *
271      * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
272      */
273     public void forgetIdentity(SubjectContext subjectContext) {
274         if (WebUtils.isHttp(subjectContext)) {
275             HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
276             HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
277             forgetIdentity(request, response);
278         }
279     }
280 
281     /**
282      * Removes the rememberMe cookie from the given request/response pair.
283      *
284      * @param request  the incoming HTTP servlet request
285      * @param response the outgoing HTTP servlet response
286      */
287     private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
288         getCookie().removeFrom(request, response);
289     }
290 }
291