写点什么

Security RememberMe 原理分析

作者:周杰伦本人
  • 2022 年 7 月 02 日
  • 本文字数:9391 字

    阅读完需:约 31 分钟

Security RememberMe 原理分析

当我们配置


.rememberMe().key("xiepanapn")
复制代码


实际是引入了配置类 RememberMeConfigurer,对于 RememberMeConfigurer 而言,最重要的是 init 方法和 configure 方法


@Overridepublic 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 加入到过滤器链中。


@Overridepublic 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); }
}
复制代码


  1. 请求到达时,先判断 SecurityContextHolder 中是否有值,没有表示用户尚未登录,此时调用 autoLogin 进行自动登录。

  2. 自动登录成功后返回 rememberMeAuth,不为 null 时表示自动登录成功,此时调用 authenticate 方法对 key 进行校验,将登录成功的用户信息保存到 SecurityContextHolder 中,然后调用登录成功回调,发布登录成功事件。

  3. 如果自动登录失败,调用 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; } }}
复制代码


  1. 调用 extractRememberMeCookie 方法从当前请求中提取出需要的 Cookie 信息。rememberMeCookie 为 null 返回 null,长度为 0 取消 cookie,将 remember-me 的值设置为 null

  2. 调用 decodeCookie 方法对获取的令牌进行解析,解析的结果是第一部分是当前登录的用户名,第二部分是时间戳,第三部分是签名,提取出来组成一个数组。

  3. 调用 processAutoLoginCookie 方法来对 Cookie 进行验证,验证通过返回用户信息,由子类实现

  4. 最后调用 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; } } }}
复制代码


  1. 判断 cookieTokens 长度是否为 3 不是 3 格式不对 直接抛出异常

  2. 从 cookieTokens 数组中提取第一项,判断是否过期

  3. 获取第 0 项,得到用户名查询当前用户信息

  4. 调用 makeTokenSignature 生成签名,签名生成方式:username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey()组成字符串进行 MD5 加密

  5. 判断第 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) + "'"); }
}}
复制代码


  1. 获取用户名密码信息 密码没有从数据库重新加载密码

  2. 计算出令牌的过期时间,令牌默认有效期是 14 天

  3. 根据令牌过期时间,用户名密码计算签名

  4. 调用 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()); } }}
复制代码


  1. cookieTokens 数组长度为 2 第 0 项为 series 第 1 项为 token

  2. 提取出两项数据根据 series 查询数据库,token 不相同说明自动登录令牌已泄露,此时移除所有自动登录记录,抛出异常

  3. 根据数据库中查询出来的结果判断是否过期

  4. 生成新的 PersistentRememberMeToken 用户名和 series 不变,更改 token 和当前时间,修改数据库

  5. 调用 addCookie 添加 Cookie,addCookie 方法中调用 setCookie 设置

  6. 感觉用户名查询用户对象并返回

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 编码

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2020.02.29 加入

公众号《盼盼小课堂》,多平台优质博主

评论

发布
暂无评论
Security RememberMe原理分析_7月月更_周杰伦本人_InfoQ写作社区