001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.web.mgt; 020 021import java.util.function.Supplier; 022 023import org.apache.shiro.lang.codec.Base64; 024import org.apache.shiro.mgt.AbstractRememberMeManager; 025import org.apache.shiro.subject.Subject; 026import org.apache.shiro.subject.SubjectContext; 027import org.apache.shiro.web.servlet.Cookie; 028import org.apache.shiro.web.servlet.ShiroHttpServletRequest; 029import org.apache.shiro.web.servlet.SimpleCookie; 030import org.apache.shiro.web.subject.WebSubject; 031import org.apache.shiro.web.subject.WebSubjectContext; 032import org.apache.shiro.web.util.WebUtils; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import javax.servlet.ServletRequest; 037import javax.servlet.http.HttpServletRequest; 038import javax.servlet.http.HttpServletResponse; 039 040 041/** 042 * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie} 043 * for later retrieval. 044 * <p/> 045 * Cookie attributes (path, domain, maxAge, etc.) may be set on this class's default 046 * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies 047 * created by this implementation. 048 * <p/> 049 * The default cookie has the following attribute values set: 050 * <table> 051 * <tr> 052 * <th>Attribute Name</th> 053 * <th>Value</th> 054 * </tr> 055 * <tr><td>{@link Cookie#getName() name}</td> 056 * <td>{@code rememberMe}</td> 057 * </tr> 058 * <tr> 059 * <td>{@link Cookie#getPath() path}</td> 060 * <td>{@code /}</td> 061 * </tr> 062 * <tr> 063 * <td>{@link Cookie#getMaxAge() maxAge}</td> 064 * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td> 065 * </tr> 066 * </table> 067 * <p/> 068 * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization 069 * and encryption logic, this class utilizes both for added security before setting the cookie value. 070 * 071 * @since 1.0 072 */ 073public class CookieRememberMeManager extends AbstractRememberMeManager { 074 075 /** 076 * The default name of the underlying rememberMe cookie which is {@code rememberMe}. 077 */ 078 public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe"; 079 080 private static final Logger LOGGER = LoggerFactory.getLogger(CookieRememberMeManager.class); 081 082 private Cookie cookie; 083 084 /** 085 * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template. 086 */ 087 public CookieRememberMeManager() { 088 setCookie(createDefaultCookie()); 089 } 090 091 /** 092 * Constructor. Pass keySupplier that supplies encryption key 093 * 094 * @param keySupplier 095 * @since 2.0 096 */ 097 public CookieRememberMeManager(Supplier<byte[]> keySupplier) { 098 super(keySupplier); 099 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}