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; 039import static org.apache.shiro.session.mgt.DefaultSessionManager.SECURE_COOKIE_DISABLED; 040 041 042/** 043 * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie} 044 * for later retrieval. 045 * <p/> 046 * Cookie attributes (path, domain, maxAge, etc.) may be set on this class's default 047 * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies 048 * created by this implementation. 049 * <p/> 050 * The default cookie has the following attribute values set: 051 * <table> 052 * <tr> 053 * <th>Attribute Name</th> 054 * <th>Value</th> 055 * </tr> 056 * <tr><td>{@link Cookie#getName() name}</td> 057 * <td>{@code rememberMe}</td> 058 * </tr> 059 * <tr> 060 * <td>{@link Cookie#getPath() path}</td> 061 * <td>{@code /}</td> 062 * </tr> 063 * <tr> 064 * <td>{@link Cookie#getMaxAge() maxAge}</td> 065 * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td> 066 * </tr> 067 * </table> 068 * <p/> 069 * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization 070 * and encryption logic, this class utilizes both for added security before setting the cookie value. 071 * 072 * @since 1.0 073 */ 074public class CookieRememberMeManager extends AbstractRememberMeManager { 075 076 /** 077 * The default name of the underlying rememberMe cookie which is {@code rememberMe}. 078 */ 079 public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe"; 080 081 private static final Logger LOGGER = LoggerFactory.getLogger(CookieRememberMeManager.class); 082 083 private Cookie cookie; 084 085 /** 086 * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template. 087 */ 088 public CookieRememberMeManager() { 089 setCookie(createDefaultCookie()); 090 } 091 092 /** 093 * Constructor. Pass keySupplier that supplies encryption key 094 * 095 * @param keySupplier 096 * @since 2.0 097 */ 098 public CookieRememberMeManager(Supplier<byte[]> keySupplier) { 099 super(keySupplier); 100 setCookie(createDefaultCookie()); 101 } 102 103 /** 104 * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by 105 * this {@code RememberMeManager}. Outgoing cookies will match this one except for the 106 * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime. 107 * <p/> 108 * Please see the class-level JavaDoc for the default cookie's attribute values. 109 * 110 * @return the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by 111 * this {@code RememberMeManager}. 112 */ 113 public Cookie getCookie() { 114 return cookie; 115 } 116 117 /** 118 * Sets the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by 119 * this {@code RememberMeManager}. Outgoing cookies will match this one except for the 120 * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime. 121 * <p/> 122 * Please see the class-level JavaDoc for the default cookie's attribute values. 123 * 124 * @param cookie the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created 125 * by this {@code RememberMeManager}. 126 */ 127 @SuppressWarnings({"UnusedDeclaration"}) 128 public void setCookie(Cookie cookie) { 129 this.cookie = cookie; 130 } 131 132 /** 133 * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value. 134 * <p/> 135 * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair 136 * so an HTTP cookie can be set on the outgoing response. If it is not a {@code WebSubject} or that 137 * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing. 138 * 139 * @param subject the Subject for which the identity is being serialized. 140 * @param serialized the serialized bytes to be persisted. 141 */ 142 protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { 143 144 if (!WebUtils.isHttp(subject)) { 145 if (LOGGER.isDebugEnabled()) { 146 String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " 147 + "request and response in order to set the rememberMe cookie. Returning immediately and " 148 + "ignoring rememberMe operation."; 149 LOGGER.debug(msg); 150 } 151 return; 152 } 153 154 155 HttpServletRequest request = WebUtils.getHttpRequest(subject); 156 HttpServletResponse response = WebUtils.getHttpResponse(subject); 157 158 //base 64 encode it and store as a cookie: 159 String base64 = Base64.encodeToString(serialized); 160 161 //the class attribute is really a template for the outgoing cookies 162 Cookie template = getCookie(); 163 Cookie cookie = new SimpleCookie(template); 164 cookie.setValue(base64); 165 cookie.saveTo(request, response); 166 } 167 168 169 private boolean isIdentityRemoved(WebSubjectContext subjectContext) { 170 ServletRequest request = subjectContext.resolveServletRequest(); 171 if (request != null) { 172 Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY); 173 return removed != null && removed; 174 } 175 return false; 176 } 177 178 179 /** 180 * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired. 181 * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte 182 * array. 183 * <p/> 184 * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP 185 * Request/Response pair so an HTTP cookie can be retrieved from the incoming request. If it is not a 186 * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this 187 * implementation returns {@code null}. 188 * 189 * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that 190 * is being used to construct a {@link Subject} instance. To be used to assist with data 191 * lookup. 192 * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired. 193 */ 194 protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { 195 196 if (!WebUtils.isHttp(subjectContext)) { 197 if (LOGGER.isDebugEnabled()) { 198 String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " 199 + "servlet request and response in order to retrieve the rememberMe cookie. Returning " 200 + "immediately and ignoring rememberMe operation."; 201 LOGGER.debug(msg); 202 } 203 return null; 204 } 205 206 WebSubjectContext wsc = (WebSubjectContext) subjectContext; 207 if (isIdentityRemoved(wsc)) { 208 return null; 209 } 210 211 HttpServletRequest request = WebUtils.getHttpRequest(wsc); 212 HttpServletResponse response = WebUtils.getHttpResponse(wsc); 213 214 String base64 = getCookie().readValue(request, response); 215 // Browsers do not always remove cookies immediately (SHIRO-183) 216 // ignore cookies that are scheduled for removal 217 if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) { 218 return null; 219 } 220 221 if (base64 != null) { 222 base64 = ensurePadding(base64); 223 if (LOGGER.isTraceEnabled()) { 224 LOGGER.trace("Acquired Base64 encoded identity [" + base64 + "]"); 225 } 226 byte[] decoded; 227 try { 228 decoded = Base64.decode(base64); 229 } catch (RuntimeException rtEx) { 230 /* 231 * https://issues.apache.org/jira/browse/SHIRO-766: 232 * If the base64 string cannot be decoded, just assume there is no valid cookie value. 233 * */ 234 getCookie().removeFrom(request, response); 235 LOGGER.warn("Unable to decode existing base64 encoded entity: [" + base64 + "].", rtEx); 236 return null; 237 } 238 239 if (LOGGER.isTraceEnabled()) { 240 LOGGER.trace("Base64 decoded byte array length: " + decoded.length + " bytes."); 241 } 242 return decoded; 243 } else { 244 //no cookie set - new site visitor? 245 return null; 246 } 247 } 248 249 /** 250 * Sometimes a user agent will send the rememberMe cookie value without padding, 251 * most likely because {@code =} is a separator in the cookie header. 252 * <p/> 253 * Contributed by Luis Arias. Thanks Luis! 254 * 255 * @param base64 the base64 encoded String that may need to be padded 256 * @return the base64 String padded if necessary. 257 */ 258 protected String ensurePadding(String base64) { 259 int length = base64.length(); 260 if (length % 4 != 0) { 261 StringBuilder sb = new StringBuilder(base64); 262 while (sb.length() % 4 != 0) { 263 sb.append('='); 264 } 265 base64 = sb.toString(); 266 } 267 return base64; 268 } 269 270 /** 271 * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair. 272 * <p/> 273 * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair. 274 * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this 275 * implementation does nothing. 276 * 277 * @param subject the subject instance for which identity data should be forgotten from the underlying persistence 278 */ 279 protected void forgetIdentity(Subject subject) { 280 if (WebUtils.isHttp(subject)) { 281 HttpServletRequest request = WebUtils.getHttpRequest(subject); 282 HttpServletResponse response = WebUtils.getHttpResponse(subject); 283 forgetIdentity(request, response); 284 } 285 } 286 287 /** 288 * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair. 289 * <p/> 290 * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP 291 * Request/Response pair. If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not 292 * have an HTTP Request/Response pair, this implementation does nothing. 293 * 294 * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation 295 */ 296 public void forgetIdentity(SubjectContext subjectContext) { 297 if (WebUtils.isHttp(subjectContext)) { 298 HttpServletRequest request = WebUtils.getHttpRequest(subjectContext); 299 HttpServletResponse response = WebUtils.getHttpResponse(subjectContext); 300 forgetIdentity(request, response); 301 } 302 } 303 304 /** 305 * Removes the rememberMe cookie from the given request/response pair. 306 * 307 * @param request the incoming HTTP servlet request 308 * @param response the outgoing HTTP servlet response 309 */ 310 private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) { 311 getCookie().removeFrom(request, response); 312 } 313 314 private Cookie createDefaultCookie() { 315 Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME); 316 cookie.setHttpOnly(true); 317 if (!Boolean.getBoolean(SECURE_COOKIE_DISABLED)) { 318 cookie.setSecure(true); 319 } 320 //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited 321 //in a year: 322 cookie.setMaxAge(Cookie.ONE_YEAR); 323 return cookie; 324 } 325}