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