Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
CookieRememberMeManager |
|
| 3.2;3.2 |
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 | 2 | 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 | 40 | public CookieRememberMeManager() { |
88 | 40 | Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME); |
89 | 40 | 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 | 40 | cookie.setMaxAge(Cookie.ONE_YEAR); |
93 | 40 | this.cookie = cookie; |
94 | 40 | } |
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 | 30 | 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 | 4 | this.cookie = cookie; |
123 | 4 | } |
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 | 2 | if (!WebUtils.isHttp(subject)) { |
138 | 0 | if (log.isDebugEnabled()) { |
139 | 0 | 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 | 0 | log.debug(msg); |
143 | } | |
144 | 0 | return; |
145 | } | |
146 | ||
147 | ||
148 | 2 | HttpServletRequest request = WebUtils.getHttpRequest(subject); |
149 | 2 | HttpServletResponse response = WebUtils.getHttpResponse(subject); |
150 | ||
151 | //base 64 encode it and store as a cookie: | |
152 | 2 | String base64 = Base64.encodeToString(serialized); |
153 | ||
154 | 2 | Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies |
155 | 2 | Cookie cookie = new SimpleCookie(template); |
156 | 2 | cookie.setValue(base64); |
157 | 2 | cookie.saveTo(request, response); |
158 | 2 | } |
159 | ||
160 | private boolean isIdentityRemoved(WebSubjectContext subjectContext) { | |
161 | 18 | ServletRequest request = subjectContext.resolveServletRequest(); |
162 | 18 | if (request != null) { |
163 | 18 | Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY); |
164 | 18 | return removed != null && removed; |
165 | } | |
166 | 0 | 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 | 18 | if (!WebUtils.isHttp(subjectContext)) { |
188 | 0 | if (log.isDebugEnabled()) { |
189 | 0 | 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 | 0 | log.debug(msg); |
193 | } | |
194 | 0 | return null; |
195 | } | |
196 | ||
197 | 18 | WebSubjectContext wsc = (WebSubjectContext) subjectContext; |
198 | 18 | if (isIdentityRemoved(wsc)) { |
199 | 0 | return null; |
200 | } | |
201 | ||
202 | 18 | HttpServletRequest request = WebUtils.getHttpRequest(wsc); |
203 | 18 | HttpServletResponse response = WebUtils.getHttpResponse(wsc); |
204 | ||
205 | 18 | 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 | 18 | if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null; |
209 | ||
210 | 16 | if (base64 != null) { |
211 | 6 | base64 = ensurePadding(base64); |
212 | 6 | if (log.isTraceEnabled()) { |
213 | 6 | log.trace("Acquired Base64 encoded identity [" + base64 + "]"); |
214 | } | |
215 | 6 | byte[] decoded = Base64.decode(base64); |
216 | 6 | if (log.isTraceEnabled()) { |
217 | 6 | log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes."); |
218 | } | |
219 | 6 | return decoded; |
220 | } else { | |
221 | //no cookie set - new site visitor? | |
222 | 10 | 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 | 6 | int length = base64.length(); |
237 | 6 | if (length % 4 != 0) { |
238 | 2 | StringBuilder sb = new StringBuilder(base64); |
239 | 8 | for (int i = 0; i < length % 4; ++i) { |
240 | 6 | sb.append('='); |
241 | } | |
242 | 2 | base64 = sb.toString(); |
243 | } | |
244 | 6 | 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 | 6 | if (WebUtils.isHttp(subject)) { |
258 | 6 | HttpServletRequest request = WebUtils.getHttpRequest(subject); |
259 | 6 | HttpServletResponse response = WebUtils.getHttpResponse(subject); |
260 | 6 | forgetIdentity(request, response); |
261 | } | |
262 | 6 | } |
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 | 4 | if (WebUtils.isHttp(subjectContext)) { |
275 | 4 | HttpServletRequest request = WebUtils.getHttpRequest(subjectContext); |
276 | 4 | HttpServletResponse response = WebUtils.getHttpResponse(subjectContext); |
277 | 4 | forgetIdentity(request, response); |
278 | } | |
279 | 4 | } |
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 | 10 | getCookie().removeFrom(request, response); |
289 | 10 | } |
290 | } | |
291 |