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 }