Security RememberMe原理分析

举报
周杰伦本人 发表于 2022/05/10 17:39:55 2022/05/10
【摘要】 Security RememberMe原理分析当我们配置.rememberMe().key("xiepanapn")实际是引入了配置类RememberMeConfigurer,对于RememberMeConfigurer而言,最重要的是init方法和configure方法@Overridepublic void init(H http) throws Exception { valid...

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);
    }

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。