Security RememberMe原理分析
Security RememberMe原理分析
当我们配置
.rememberMe()
.key("xiepanapn")
实际是引入了配置类RememberMeConfigurer,对于RememberMeConfigurer而言,最重要的是init方法和configure方法
@Override
public void init(H http) throws Exception {
validateInput();
String key = getKey();
RememberMeServices rememberMeServices = getRememberMeServices(http, key);
http.setSharedObject(RememberMeServices.class, rememberMeServices);
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && this.logoutHandler != null) {
logoutConfigurer.addLogoutHandler(this.logoutHandler);
}
RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(
key);
authenticationProvider = postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider);
initDefaultLoginFilter(http);
}
首先获取key,不配置默认是UUID字符串,系统每次重启都会重新生成新的key,导致之前下发的remember-me失效,有了key之后获取RememberMeServices 根据是否有tokenRepository创建TokenBasedRememberMeServices或者PersistentRememberMeServices
private AbstractRememberMeServices createRememberMeServices(H http, String key) {
return this.tokenRepository == null
? createTokenBasedRememberMeServices(http, key)
: createPersistentRememberMeServices(http, key);
}
configure方法主要创建RememberMeAuthenticationFilter 创建时传入实例好的rememberMeServices,再将创建好的RememberMeAuthenticationFilter加入到过滤器链中。
@Override
public void configure(H http) {
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class),
this.rememberMeServices);
if (this.authenticationSuccessHandler != null) {
rememberMeFilter
.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
rememberMeFilter = postProcess(rememberMeFilter);
http.addFilter(rememberMeFilter);
}
RememberMeAuthenticationFilter
RememberMeAuthenticationFilter最重要的是doFilter方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
}
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
- 请求到达时,先判断SecurityContextHolder中是否有值,没有表示用户尚未登录,此时调用autoLogin进行自动登录。
- 自动登录成功后返回rememberMeAuth,不为null时表示自动登录成功,此时调用authenticate方法对key进行校验,将登录成功的用户信息保存到SecurityContextHolder中,然后调用登录成功回调,发布登录成功事件。
- 如果自动登录失败,调用rememberMeServices.loginFail方法处理登录失败逻辑
重点看一下autoLogin
AbstractRememberMeServices
接口为RememberMeServices
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest var1, HttpServletResponse var2);
void loginFail(HttpServletRequest var1, HttpServletResponse var2);
void loginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
}
AbstractRememberMeServices实现了RememberMeServices接口
实现autoLogin方法:主要功能是从当前请求中提取出令牌信息,根据令牌信息完成自动登录功能,登录成功后返回Authentication对象
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
} catch (UsernameNotFoundException var7) {
this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
} catch (InvalidCookieException var8) {
this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
} catch (AccountStatusException var9) {
this.logger.debug("Invalid UserDetails: " + var9.getMessage());
} catch (RememberMeAuthenticationException var10) {
this.logger.debug(var10.getMessage());
}
this.cancelCookie(request, response);
return null;
}
}
}
- 调用extractRememberMeCookie方法从当前请求中提取出需要的Cookie信息。rememberMeCookie为null返回null,长度为0取消cookie,将remember-me的值设置为null
- 调用decodeCookie方法对获取的令牌进行解析,解析的结果是第一部分是当前登录的用户名,第二部分是时间戳,第三部分是签名,提取出来组成一个数组。
- 调用processAutoLoginCookie方法来对Cookie进行验证,验证通过返回用户信息,由子类实现
- 最后调用createSuccessfulAuthentication创建登录成功的用户对象,类型为RememberMeAuthenticationToken ,与用户名密码登录的用户对象不同UsernamePasswordAuthenticationToken
登录失败取消Cookie的设置,登录成功调用rememberMeRequested判断当前请求是否开启了自动登录请求,如果开启自动登录调用onLoginSuccess方法,由子类实现
ublic final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
判断当前请求是否开启了自动登录请求:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
String paramValue = request.getParameter(parameter);
if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
}
return false;
}
}
}
服务端配置alwaysRemember是否为true 前端传来的remember-me值是否为on yes true 1
登录成功后会调用setCookie方法:
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
String cookieValue = this.encodeCookie(tokens);
Cookie cookie = new Cookie(this.cookieName, cookieValue);
cookie.setMaxAge(maxAge);
cookie.setPath(this.getCookiePath(request));
if (this.cookieDomain != null) {
cookie.setDomain(this.cookieDomain);
}
if (maxAge < 1) {
cookie.setVersion(1);
}
if (this.useSecureCookie == null) {
cookie.setSecure(request.isSecure());
} else {
cookie.setSecure(this.useSecureCookie);
}
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
protected String encodeCookie(String[] cookieTokens) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < cookieTokens.length; ++i) {
try {
sb.append(URLEncoder.encode(cookieTokens[i], StandardCharsets.UTF_8.toString()));
} catch (UnsupportedEncodingException var5) {
this.logger.error(var5.getMessage(), var5);
}
if (i < cookieTokens.length - 1) {
sb.append(":");
}
}
String value = sb.toString();
sb = new StringBuilder(new String(Base64.getEncoder().encode(value.getBytes())));
while(sb.charAt(sb.length() - 1) == '=') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
对前端传过来的数据进行编码,将数组用冒号分隔开,Base64转编码为字符串 设置到Cookie中。
AbstractRememberMeServices有两个子类 TokenBasedRememberMeServices 和 持久化Token令牌类PersistentTokenBasedRememberMeServices
TokenBasedRememberMeServices
实现processAutoLoginCookie方法和onLoginSuccess方法
processAutoLoginCookie
验证Cookie中的令牌是否合法
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 3) {
throw new InvalidCookieException("Cookie token did not contain 3 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
long tokenExpiryTime;
try {
tokenExpiryTime = new Long(cookieTokens[1]);
} catch (NumberFormatException var8) {
throw new InvalidCookieException("Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1] + "')");
}
if (this.isTokenExpired(tokenExpiryTime)) {
throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime) + "'; current time is '" + new Date() + "')");
} else {
UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(cookieTokens[0]);
Assert.notNull(userDetails, () -> {
return "UserDetailsService " + this.getUserDetailsService() + " returned null for username " + cookieTokens[0] + ". This is an interface contract violation";
});
String expectedTokenSignature = this.makeTokenSignature(tokenExpiryTime, userDetails.getUsername(), userDetails.getPassword());
if (!equals(expectedTokenSignature, cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'");
} else {
return userDetails;
}
}
}
}
- 判断cookieTokens长度是否为3 不是3格式不对 直接抛出异常
- 从cookieTokens数组中提取第一项,判断是否过期
- 获取第0项,得到用户名查询当前用户信息
- 调用makeTokenSignature生成签名,签名生成方式:username + “:” + tokenExpiryTime + “:” + password + “:” + this.getKey()组成字符串进行MD5加密
- 判断第4步生成的签名与传进来的签名是否相同
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey();
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException var8) {
throw new IllegalStateException("No MD5 algorithm available!");
}
return new String(Hex.encode(digest.digest(data.getBytes())));
}
onLoginSuccess
登录成功方法
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = this.retrieveUserName(successfulAuthentication);
String password = this.retrievePassword(successfulAuthentication);
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
} else {
if (!StringUtils.hasLength(password)) {
UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
String signatureValue = this.makeTokenSignature(expiryTime, username, password);
this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
}
- 获取用户名密码信息 密码没有从数据库重新加载密码
- 计算出令牌的过期时间,令牌默认有效期是14天
- 根据令牌过期时间,用户名密码计算签名
- 调用setCookie方法设置Cookie,分别传入用户名过期时间签名,在setCookie中数组转字符串并进行Base64编码
总结:
当用户通过用户名密码登录后,系统根据用户的用户名密码以及令牌过期时间计算出一个签名,这个签名用MD5加密,不可逆。然后用户名,令牌过期时间,签名拼接成一个字符串,中间用冒号分隔开,对拼接好的字符串进行Base64编码,然后将编码后的结果返回给前端,这既是令牌。当用户关闭浏览器再次打开,访问系统资源会自动携带Cookie信息,服务器拿到Cookie中的令牌,先进行Base64解码,解码后提取出令牌的三项数据;接着根据令牌的数据判断是否过期,没有过期查询出用户信息,计算出签名与令牌中的签名对比,一致表示令牌合法,自动登录成功,否则自动登录失败。
PersistentTokenBasedRememberMeServices
持久化Token 存储的数据
processAutoLoginCookie
验证Cookie中的令牌是否合法
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
- cookieTokens数组长度为2 第0项为series 第1项为token
- 提取出两项数据根据series查询数据库,token不相同说明自动登录令牌已泄露,此时移除所有自动登录记录,抛出异常
- 根据数据库中查询出来的结果判断是否过期
- 生成新的PersistentRememberMeToken 用户名和series不变,更改token和当前时间,修改数据库
- 调用addCookie添加Cookie,addCookie方法中调用setCookie设置
- 感觉用户名查询用户对象并返回
onLoginSuccess
登录成功方法
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
登录成功后构建PersistentRememberMeToken series和token是随机生成的,然后生成的对象存入数据库中,调用addCookie方法添加相关的Cookie信息。
PersistentTokenBasedRememberMeServices和TokenBasedRememberMeServices区别:
PersistentTokenBasedRememberMeServices返回前端的令牌是series和token组成的字符串进行Base64编码
TokenBasedRememberMeServices返回的前端的令牌是用户名过期时间和签名组成的字符串进行Base64编码
- 点赞
- 收藏
- 关注作者
评论(0)