1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
public class TokenBasedRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
//~ Static fields/initializers =====================================================================================
public static final String ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY = "ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE";
public static final String DEFAULT_PARAMETER = "_acegi_security_remember_me";
protected static final Log logger = LogFactory.getLog(TokenBasedRememberMeServices.class);
//~ Instance fields ================================================================================================
private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
private String key;
private String parameter = DEFAULT_PARAMETER;
private UserDetailsService userDetailsService;
private long tokenValiditySeconds = 1209600; // 14 days
private boolean alwaysRemember = false;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(key);
Assert.hasLength(parameter);
Assert.notNull(userDetailsService);
}
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
if ((cookies == null) || (cookies.length == 0)) {
return null;
}
for (int i = 0; i < cookies.length; i++) {
if (ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY.equals(cookies[i].getName())) {
String cookieValue = cookies[i].getValue();
if (Base64.isArrayByteBase64(cookieValue.getBytes())) {
if (logger.isDebugEnabled()) {
logger.debug("Remember-me cookie detected");
}
// Decode token from Base64
// format of token is:
// username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
String cookieAsPlainText = new String(Base64.decodeBase64(cookieValue.getBytes()));
String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":");
if (cookieTokens.length == 3) {
long tokenExpiryTime;
try {
tokenExpiryTime = new Long(cookieTokens[1]).longValue();
} catch (NumberFormatException nfe) {
cancelCookie(request, response,
"Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1] + "')");
return null;
}
// Check it has not expired
if (tokenExpiryTime < System.currentTimeMillis()) {
cancelCookie(request, response,
"Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
+ "'; current time is '" + new Date() + "')");
return null;
}
// Check the user exists
// Defer lookup until after expiry time checked, to possibly avoid expensive lookup
UserDetails userDetails;
try {
userDetails = this.userDetailsService.loadUserByUsername(cookieTokens[0]);
} catch (UsernameNotFoundException notFound) {
cancelCookie(request, response,
"Cookie token[0] contained username '" + cookieTokens[0] + "' but was not found");
return null;
}
// Immediately reject if the user is not allowed to login
if (!userDetails.isAccountNonExpired() || !userDetails.isCredentialsNonExpired()
|| !userDetails.isEnabled()) {
cancelCookie(request, response,
"Cookie token[0] contained username '" + cookieTokens[0]
+ "' but account has expired, credentials have expired, or user is disabled");
return null;
}
// Check signature of token matches remaining details
// Must do this after user lookup, as we need the DAO-derived password
// If efficiency was a major issue, just add in a UserCache implementation,
// but recall this method is usually only called one per HttpSession
// (as if the token is valid, it will cause SecurityContextHolder population, whilst
// if invalid, will cause the cookie to be cancelled)
String expectedTokenSignature = DigestUtils.md5Hex(userDetails.getUsername() + ":"
+ tokenExpiryTime + ":" + userDetails.getPassword() + ":" + this.key);
if (!expectedTokenSignature.equals(cookieTokens[2])) {
cancelCookie(request, response,
"Cookie token[2] contained signature '" + cookieTokens[2] + "' but expected '"
+ expectedTokenSignature + "'");
return null;
}
// By this stage we have a valid token
if (logger.isDebugEnabled()) {
logger.debug("Remember-me cookie accepted");
}
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, userDetails,
userDetails.getAuthorities());
auth.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
return auth;
} else {
cancelCookie(request, response,
"Cookie token did not contain 3 tokens; decoded value was '" + cookieAsPlainText + "'");
return null;
}
} else {
cancelCookie(request, response,
"Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
return null;
}
}
}
return null;
}
private void cancelCookie(HttpServletRequest request, HttpServletResponse response, String reasonForLog) {
if ((reasonForLog != null) && logger.isDebugEnabled()) {
logger.debug("Cancelling cookie for reason: " + reasonForLog);
}
response.addCookie(makeCancelCookie(request));
}
public String getKey() {
return key;
}
public String getParameter() {
return parameter;
}
public long getTokenValiditySeconds() {
return tokenValiditySeconds;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void loginFail(HttpServletRequest request, HttpServletResponse response) {
cancelCookie(request, response, "Interactive authentication attempt was unsuccessful");
}
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
return RequestUtils.getBooleanParameter(request, parameter, false);
}
public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// Exit if the principal hasn't asked to be remembered
if (!rememberMeRequested(request, parameter)) {
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set parameter '" + this.parameter
+ "')");
}
return;
}
// Determine username and password, ensuring empty strings
Assert.notNull(successfulAuthentication.getPrincipal());
Assert.notNull(successfulAuthentication.getCredentials());
String username;
String password;
if (successfulAuthentication.getPrincipal() instanceof UserDetails) {
username = ((UserDetails) successfulAuthentication.getPrincipal()).getUsername();
password = ((UserDetails) successfulAuthentication.getPrincipal()).getPassword();
} else {
username = successfulAuthentication.getPrincipal().toString();
password = successfulAuthentication.getCredentials().toString();
}
Assert.hasLength(username);
Assert.hasLength(password);
long expiryTime = System.currentTimeMillis() + (tokenValiditySeconds * 1000);
// construct token to put in cookie; format is:
// username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
String signatureValue = new String(DigestUtils.md5Hex(username + ":" + expiryTime + ":" + password + ":" + key));
String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
String tokenValueBase64 = new String(Base64.encodeBase64(tokenValue.getBytes()));
response.addCookie(makeValidCookie(expiryTime, tokenValueBase64, request));
if (logger.isDebugEnabled()) {
logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
cancelCookie(request, response, "Logout of user " + authentication.getName());
}
protected Cookie makeCancelCookie(HttpServletRequest request) {
Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
return cookie;
}
protected Cookie makeValidCookie(long expiryTime, String tokenValueBase64, HttpServletRequest request) {
Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, tokenValueBase64);
cookie.setMaxAge(60 * 60 * 24 * 365 * 5); // 5 years
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
return cookie;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setKey(String key) {
this.key = key;
}
public void setParameter(String parameter) {
this.parameter = parameter;
}
public void setTokenValiditySeconds(long tokenValiditySeconds) {
this.tokenValiditySeconds = tokenValiditySeconds;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public boolean isAlwaysRemember() {
return alwaysRemember;
}
public void setAlwaysRemember(boolean alwaysRemember) {
this.alwaysRemember = alwaysRemember;
}
} |
Partager