跳到主要内容

15、Spring Security 实战 - SpringSecurity 记住我 RememberMe

1. RememberMe 基本使用

RememberMe 是一种服务器端的行为。传统的登录方式基于Session会话,一旦用户的会话超时过期,就要再次登录,这样太过于烦琐。如果能有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多,RememberMe 就是为了解决这一需求而生的。

具体的实现思路就是通过 Cookie 来记录当前用户身份。当用户登录成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头带回前端存储在cookie中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie中的信息进行校验分析,进而确定出用户的身份,Cookie中所保存的用户信息也是有时效的,例如三天、一周等。

配置SpringSecurity 的记住我功能:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User
                .withUsername("root")
                .password("{noop}123")
                .roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                // 开启rememberme功能
                .rememberMe()
                .and()
                .csrf().disable();
    }
}

配置session在1分钟之后过期:

server.servlet.session.timeout=1

可以看到一旦打开了记住我功能,登录⻚面中会多出一个 RememberMe 选项:
 

2. RememberMe 登录流程源码分析

步骤1:AbstractAuthenticationProcessingFilter#doFilter 登录请求认证

登录请求首先被AbstractAuthenticationProcessingFilter过滤器拦截:

 

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
   
     
        if (!requiresAuthentication(request, response)) {
   
     
            chain.doFilter(request, response);
            return;
        }
        try {
   
     
            // 1、调用子类的attemptAuthentication方法尝试认证
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
   
     
                return;
            }
            // 2、会话处理
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            if (this.continueChainBeforeSuccessfulAuthentication) {
   
     
                chain.doFilter(request, response);
            }
            // 3、认证成功的处理
            successfulAuthentication(request, response, chain, authenticationResult);
        }catch (InternalAuthenticationServiceException failed) {
   
     
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            // 4、认证失败的处理
            unsuccessfulAuthentication(request, response, failed);
        }catch (AuthenticationException ex) {
   
     
            // 认证失败的处理
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}

步骤2:AbstractAuthenticationProcessingFilter#successfulAuthentication 认证成功处理

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {
   
     
        // 1、将认证成功的Authentication对象存入SecurityContextHolder
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);
        if (this.logger.isDebugEnabled()) {
   
     
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }
        // 2、如果开启了记住我功能,当身份验证成功时调用
        this.rememberMeServices.loginSuccess(request, response, authResult);
        // 3、发布事件
        if (this.eventPublisher != null) {
   
     
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        // 4、认证成功的回调
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
}

步骤3:AbstractRememberMeServices#loginSuccess 认证成功时调用

public abstract class AbstractRememberMeServices
    implements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {
   
     

    public static final String DEFAULT_PARAMETER = "remember-me";
    private String parameter = DEFAULT_PARAMETER;

    @Override
    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {
   
     

        // 检查传入请求并检查是否存在已配置的“记住我”参数。 
        // 如果它存在,或者如果 alwaysRemember 设置为 true,则调用 onLoginSucces。
        if (!rememberMeRequested(request, this.parameter)) {
   
     
            this.logger.debug("Remember-me login not requested.");
            return;
        }
        onLoginSuccess(request, response, successfulAuthentication);
    }

   /**
    * 判断请求中是否存在已配置的记住我remember-me参数,或者alwaysRemember是否为true
    */
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
   
     
        if (this.alwaysRemember) {
   
     
            return true;
        }
        // paramValue=on
        String paramValue = request.getParameter(parameter);
        if (paramValue != null) {
   
     
            if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
   
     
                return true;
            }
        }
        this.logger.debug(
            LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
        return false;
    }
}

步骤4:TokenBasedRememberMeServices#onLoginSuccess

public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
   
     

    // 令牌的过期时间默认值为两周
    public static final int TWO_WEEKS_S = 1209600;
    private int tokenValiditySeconds = TWO_WEEKS_S;
    private Boolean useSecureCookie = null;

    @Override
    public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
                               Authentication successfulAuthentication) {
   
     
        // 获取用户名
        String username = retrieveUserName(successfulAuthentication);
        // 获取密码
        String password = retrievePassword(successfulAuthentication);

        if (!StringUtils.hasLength(username)) {
   
     
            this.logger.debug("Unable to retrieve username");
            return;
        }

        // 如果用户密码在用户登录成功后从successfulAuthentication对象中擦除
        // 则从数据库中重新加载出用户密码。
        if (!StringUtils.hasLength(password)) {
   
     
            UserDetails user = getUserDetailsService().loadUserByUsername(username);
            password = user.getPassword();
            if (!StringUtils.hasLength(password)) {
   
     
                this.logger.debug("Unable to obtain password for user: " + username);
                return;
            }
        }

        // 计算出令牌的过期时长,令牌默认有效期是两周。
        int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
        long expiryTime = System.currentTimeMillis();
        // 令牌的过期时间=当前系统时间+过期时长
        expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);

        // 根据令牌的过期时间、用户名以及用户密码,计算出一个签名
        String signatureValue = makeTokenSignature(expiryTime, username, password);

        // 调用 setCookie 方法设置 Cookie 
        // 第一个参数是一个数组,数组中一共包含三项:用户名、过期时间以及签名,
        // 在setCookie 方法中会将数组转为字符串,并进行Base64编码后响应给前端。
        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) + "'");
        }
    }

    // 从Authentication对象中获取username
    protected String retrieveUserName(Authentication authentication) {
   
     
        if (isInstanceOfUserDetails(authentication)) {
   
     
            return ((UserDetails) authentication.getPrincipal()).getUsername();
        }
        return authentication.getPrincipal().toString();
    }

    // 判断是否是UserDetails实例
    private boolean isInstanceOfUserDetails(Authentication authentication) {
   
     
        return authentication.getPrincipal() instanceof UserDetails;
    }

    // 从Authentication对象中获取password
    protected String retrievePassword(Authentication authentication) {
   
     
        if (isInstanceOfUserDetails(authentication)) {
   
     
            return ((UserDetails) authentication.getPrincipal()).getPassword();
        }
        if (authentication.getCredentials() != null) {
   
     
            return authentication.getCredentials().toString();
        }
        return null;
    }

    // 计算令牌的过期时间
    protected int calculateLoginLifetime(HttpServletRequest request, Authentication authentication) {
   
     
        return getTokenValiditySeconds();
    }

    protected int getTokenValiditySeconds() {
   
     
        return this.tokenValiditySeconds;
    }

    // 根据令牌的过期时间、用户名以及用户密码,计算出一个签名
    protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
   
     
        String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
        try {
   
     
            // 使用MD5加密算法加密
            MessageDigest digest = MessageDigest.getInstance("MD5");
            return new String(Hex.encode(digest.digest(data.getBytes())));
        }
        catch (NoSuchAlgorithmException ex) {
   
     
            throw new IllegalStateException("No MD5 algorithm available!");
        }
    }

    // 设置cookie
    protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
   
     
        String cookieValue = encodeCookie(tokens);
        
        Cookie cookie = new Cookie(this.cookieName, cookieValue);
        // 设置cookie的过期时间
        cookie.setMaxAge(maxAge);
        // 设置cookie的生效路径
        cookie.setPath(getCookiePath(request));
        if (this.cookieDomain != null) {
   
     
            cookie.setDomain(this.cookieDomain);
        }
        if (maxAge < 1) {
   
     
            cookie.setVersion(1);
        }
        cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());
        cookie.setHttpOnly(true);
        // 在响应对象中添加cookie
        response.addCookie(cookie);
    }

    // 将cookieTokensp拼接成字符串,并使用Base64编码
    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 ex) {
   
     
                this.logger.error(ex.getMessage(), ex);
            }
            if (i < cookieTokens.length - 1) {
   
     
                sb.append(DELIMITER);
            }
        }
        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();
    }

}

3. RememberMe 会话过期自动登录流程源码分析

步骤1: RememberMeAuthenticationFilter#doFilter

当在SecurityConfig 配置中开启了"记住我"功能之后,在进行认证时如果勾选了"记住我"选项,此时打开浏览器控制台,分析整个登录过程。

首先当我们登录时,在登录请求中多了一个 RememberMe 的参数:
 

很显然,这个参数就是告诉服务器应该开启 RememberMe 功能的。如果自定义登录⻚面开启RememberMe 功能应该多加入一个一样的请求参数就可以啦。

该请求会被 RememberMeAuthenticationFilter 进行拦截然后自动登录具体参⻅源码:

public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
   
     

    // 事件发布
    private ApplicationEventPublisher eventPublisher;

    // 认证成功的回调接口
    private AuthenticationSuccessHandler successHandler;

    // 认证管理器
    private AuthenticationManager authenticationManager;

    // 记住我功能
    private RememberMeServices rememberMeServices;

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
   
     
        // 请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值
        // 没值的话表示用户尚未登录;调用 autoLogin 方法进行自动登录
        // 有值的话说明用户已经登录在,直接放行;
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
   
     
            chain.doFilter(request, response);
            return;
        }
        // 调用 autoLogin 方法进行自动登录。
        Authentication rememberMeAuth 
            				= this.rememberMeServices.autoLogin(request, response);
        // 登录成功
        if (rememberMeAuth != null) {
   
     
            try {
   
     
                rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                context.setAuthentication(rememberMeAuth);
                SecurityContextHolder.setContext(context);
                onSuccessfulAuthentication(request, response, rememberMeAuth);
                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 ex) {
   
     
                this.rememberMeServices.loginFail(request, response);
                onUnsuccessfulAuthentication(request, response, ex);
            }
        }
        chain.doFilter(request, response);
    }
}

请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,有值的话说明用户已经登录在,直接放行;没值的话表示用户尚未登录;调用 autoLogin 方法进行自动登录。

当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用 authenticate 方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含 RememberMeServices 中的1oginSuccess 方法。

如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调。onUnsuccessfulAuthentication 和 onSuccessfulAuthentication都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter 过滤器所做的事情,成功将RememberMeServices的服务集成进来。

步骤2:AbstractRememberMeServices#autoLogin

public abstract class AbstractRememberMeServices
    implements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {
   
     

    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
    private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
    private static final String DELIMITER = ":";

    @Override
    public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
   
     
        // 1、提取请求中Cookie的value值
        String rememberMeCookie = extractRememberMeCookie(request);
        if (rememberMeCookie == null) {
   
     
            return null;
        }
        this.logger.debug("Remember-me cookie detected");
        if (rememberMeCookie.length() == 0) {
   
     
            this.logger.debug("Cookie was empty");
            cancelCookie(request, response);
            return null;
        }
        try {
   
     
            // 2、对CookieValue进行解码:["root", "1664974500735", "9e44c8368018d34..."]
            String[] cookieTokens = decodeCookie(rememberMeCookie);
            // 3、调用子类的方法验证 Cookie 中的令牌信息是否合法
            UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
            // 4、校验用户信息
            this.userDetailsChecker.check(user);
            // 5、创建并返回认证成功的Authentication对象
            return createSuccessfulAuthentication(request, user);
        }catch (CookieTheftException ex) {
   
     
            cancelCookie(request, response);
            throw ex;
        }
        // ...
        cancelCookie(request, response);
        return null;
    }

    // 从请求的Cookie中获取remember-me的cookieValue值
    protected String extractRememberMeCookie(HttpServletRequest request) {
   
     
        // 从请求中获取所有的Cookie
        Cookie[] cookies = request.getCookies();
        if ((cookies == null) || (cookies.length == 0)) {
   
     
            return null;
        }
        for (Cookie cookie : cookies) {
   
     
            // 判断Cookie的名称是否为remember-me,如果是就获取cookie的value
            if (this.cookieName.equals(cookie.getName())) {
   
     
                return cookie.getValue();
            }
        }
        return null;
    }

    // 对cookieValue进行base64解密
    protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
   
     
        for (int j = 0; j < cookieValue.length() % 4; j++) {
   
     
            cookieValue = cookieValue + "=";
        }
        String cookieAsPlainText;
        try {
   
     
            // cookieAsPlainText:"root:1664974500735:9e44c8368018d34940dd599e422e2e3c"
            cookieAsPlainText 
                = new String(Base64.getDecoder().decode(cookieValue.getBytes()));
        }
        catch (IllegalArgumentException ex) {
   
     
            throw new InvalidCookieException("Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
        }
        // 对cookieAsPlainText使用冒号(:)分割
        String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
        for (int i = 0; i < tokens.length; i++) {
   
     
            try {
   
     
                tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString());
            }
            catch (UnsupportedEncodingException ex) {
   
     
                this.logger.error(ex.getMessage(), ex);
            }
        }
        return tokens;
    }

    protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
   
     
        RememberMeAuthenticationToken auth 
           	 = new RememberMeAuthenticationToken(this.key, user,
                                                                         this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
        return auth;
    }
}

步骤3:TokenBasedRememberMeServices#processAutoLoginCookie

在开启记住我后如果没有加入额外配置默认实现就是由TokenBasedRememberMeServices
进行的实现。查看这个类源码中 processAutoLoginCookie 方法实现:

public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
   
     

    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,HttpServletResponse response) {
   
     
        // ["root", "1664974500735", "9e44c8368018d34..."]
        // 1、如果cookieTokens数组的长度不等于3则格式错误,抛出异常
        if (cookieTokens.length != 3) {
   
     
            throw new InvalidCookieException(
                "Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }
        // 2、获取cookieTokens数组中index=1处的值,即token的过期时间:1664974500735
        long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
        // 3、判断令牌是否过期,如果己经过期,则拋出异常。
        if (isTokenExpired(tokenExpiryTime)) {
   
     
            throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
                                             + "'; current time is '" + new Date() + "')");
        }
        // 4、根据用户名(cookieTokens数组的第1项)查询出当前用户对象
        UserDetails userDetails 
            = getUserDetailsService().loadUserByUsername(cookieTokens[0]);

        Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
                       + " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");

		// 5、生成一个签名:"9e44c8368018d34940dd599e422e2e3c"
        String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),userDetails.getPassword());

        // 6、判断cookieTokens[2]中的签名和生成的签名是否相等,如果不相等则抛出异常,相等则令牌合法
        if (!equals(expectedTokenSignature, cookieTokens[2])) {
   
     
            throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
                                             + "' but expected '" + expectedTokenSignature + "'");
        }
        return userDetails;
    }

    // 获取token的过期时间
    private long getTokenExpiryTime(String[] cookieTokens) {
   
     
        try {
   
     
            // 获取cookieTokens数组中的第二个值,并将其转为Long类型
            return new Long(cookieTokens[1]);
        }
        catch (NumberFormatException nfe) {
   
     
            throw new InvalidCookieException(
                "Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1] + "')");
        }
    }

    protected boolean isTokenExpired(long tokenExpiryTime) {
   
     
        return tokenExpiryTime < System.currentTimeMillis();
    }

    // 计算要放入cookie中的数字签名,默认值为 MD5 ("username:tokenExpiryTime:password:key")
    protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
   
     
        //首先将用户名、令牌过期时间、用户密码以及 key 组成一个宇符串,中间用“:”隔开
        String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
        try {
   
     
            MessageDigest digest = MessageDigest.getInstance("MD5");
            // 然后通过MD5 消息摘要算法对该宇符串进行加密,将加密结果转为一个字符串返回;
            return new String(Hex.encode(digest.digest(data.getBytes())));
        }catch (NoSuchAlgorithmException ex) {
   
     
            throw new IllegalStateException("No MD5 algorithm available!");
        }
    }
}

4. RememberMe 原理

 

当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名、令牌过期时间以及签名拼接成一个字符串,中间用“:” 隔开,对拼接好的字符串进行Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。

当会话过期之后,访问系统资源时会自动携带上Cookie中的令牌,服务端拿到 Cookie中的令牌后,先进行 Bae64解码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息:接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败。

5. RememberMeServices

public interface RememberMeServices {
   
     

    // 从请求中提取出需要的参数,完成自动登录功能;
   Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

   // 登录失败的回调;
   void loginFail(HttpServletRequest request, HttpServletResponse response);

   // 登录成功的回调;
   void loginSuccess(HttpServletRequest request, HttpServletResponse response,
         Authentication successfulAuthentication);

}

 

6. 内存令牌 PersistentTokenBasedRememberMeServices

可以看到基于TokenBasedRememberMeServices实现的记住我方式是不安全的,因为我们可以直接在浏览器中看到Base64编码后的cookie信息,那么拿着这个cookie就可以任意访问系统了:
 

SpringSecurity提供了一种更为安全的记住我功能,即基于PersistentTokenBasedRememberMeServices实现记住我功能。

1. 登录流程源码变化

登录流程的步骤1、步骤2、步骤3不变,变化的是步骤4,不再调用TokenBasedRememberMeServices#onLoginSuccess,而是调用PersistentTokenBasedRememberMeServices#onLoginSuccess 。

步骤1:PersistentTokenBasedRememberMeServices#onLoginSuccess
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
   
     

    private PersistentTokenRepository tokenRepository 
        = new InMemoryTokenRepositoryImpl();

    private SecureRandom random;

    public static final int DEFAULT_SERIES_LENGTH = 16;

    public static final int DEFAULT_TOKEN_LENGTH = 16;

    private int seriesLength = DEFAULT_SERIES_LENGTH;

    private int tokenLength = DEFAULT_TOKEN_LENGTH;
    @Override
    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {
   
     
        // 获取用户名
        String username = successfulAuthentication.getName();
        // 使用构造函数生成 PersistentRememberMeToken
        PersistentRememberMeToken persistentToken 
            = new PersistentRememberMeToken(username, 
                                            generateSeriesData(),
                                            generateTokenData(), 
                                            new Date());
        try {
   
     
            // 存储PersistentRememberMeToken到内存(HashMap)
            this.tokenRepository.createNewToken(persistentToken);
            // 在响应对象中添加cookie
            addCookie(persistentToken, request, response);
        }
        catch (Exception ex) {
   
     
            this.logger.error("Failed to save persistent token ", ex);
        }
    }

    protected String generateSeriesData() {
   
     
        byte[] newSeries = new byte[this.seriesLength];
        this.random.nextBytes(newSeries);
        return new String(Base64.getEncoder().encode(newSeries));
    }

    protected String generateTokenData() {
   
     
        byte[] newToken = new byte[this.tokenLength];
        this.random.nextBytes(newToken);
        return new String(Base64.getEncoder().encode(newToken));
    }

    private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
   
     
        setCookie(new String[] {
   
      token.getSeries(), token.getTokenValue() }, 
                  getTokenValiditySeconds(), 
                  request,response);
    }

    protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
   
     
        // 对tokens使用Base64编码
        String cookieValue = encodeCookie(tokens);
        Cookie cookie = new Cookie(this.cookieName, cookieValue);
        // cookie的最大生效时间
        cookie.setMaxAge(maxAge);
        // cookie的生效路径
        cookie.setPath(getCookiePath(request));
        if (this.cookieDomain != null) {
   
     
            cookie.setDomain(this.cookieDomain);
        }
        if (maxAge < 1) {
   
     
            cookie.setVersion(1);
        }
        cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());
        cookie.setHttpOnly(true);
        // 在响应对象中添加cookie信息
        response.addCookie(cookie);
    }

    private String getCookiePath(HttpServletRequest request) {
   
     
        String contextPath = request.getContextPath();
        return (contextPath.length() > 0) ? contextPath : "/";
    }

    // 将cookieTokens数组中的值拼接成字符串,然后使用Base64编码
    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 ex) {
   
     
                this.logger.error(ex.getMessage(), ex);
            }
            if (i < cookieTokens.length - 1) {
   
     
                sb.append(DELIMITER);
            }
        }
        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();
    }
}

步骤2:PersistentRememberMeToken 源码
public class PersistentRememberMeToken {
   
     

   private final String username;

   private final String series;

   private final String tokenValue;

   private final Date date;

   public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) {
   
     
      this.username = username;
      this.series = series;
      this.tokenValue = tokenValue;
      this.date = date;
   }
}

 

步骤3:InMemoryTokenRepositoryImpl#createNewToken
public class InMemoryTokenRepositoryImpl implements PersistentTokenRepository {
   
     

   private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap<>();

  /**
   * 存储PersistentRememberMeToken到内存,也可以使用redis来存储
   */
   @Override
   public synchronized void createNewToken(PersistentRememberMeToken token) {
   
     
       // 通过series获取当前的PersistentRememberMeToken
      PersistentRememberMeToken current = this.seriesTokens.get(token.getSeries());
       // 如果PersistentRememberMeToken已经存在,抛出异常
      if (current != null) {
   
     
         throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!");
      }
       // 使用内存HashMap来存储token,当需要的时候可以直接从内存中取
       // key=series,value=PersistentRememberMeToken
      this.seriesTokens.put(token.getSeries(), token);
   }

   @Override
   public synchronized void updateToken(String series, String tokenValue, Date lastUsed) {
   
     
      PersistentRememberMeToken token = getTokenForSeries(series);
      PersistentRememberMeToken newToken 
          = new PersistentRememberMeToken(token.getUsername(), 
                                          series, 
                                          tokenValue,
                                          new Date());
      // Store it, overwriting the existing one.
      this.seriesTokens.put(series, newToken);
   }

   @Override
   public synchronized PersistentRememberMeToken getTokenForSeries(String seriesId) {
   
     
      return this.seriesTokens.get(seriesId);
   }

   @Override
   public synchronized void removeUserTokens(String username) {
   
     
      Iterator<String> series = this.seriesTokens.keySet().iterator();
      while (series.hasNext()) {
   
     
         String seriesId = series.next();
         PersistentRememberMeToken token = this.seriesTokens.get(seriesId);
         if (username.equals(token.getUsername())) {
   
     
            series.remove();
         }
      }
   }
}

2. 会话过期自动登录流程源码变化

登录流程的步骤1、步骤2不变,变化的是步骤3,不再调用TokenBasedRememberMeServices#processAutoLoginCookie,而是调用PersistentTokenBasedRememberMeServices#processAutoLoginCookie。

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
   
     

    private PersistentTokenRepository tokenRepository 
        					= new InMemoryTokenRepositoryImpl();

    private SecureRandom random;

    public static final int DEFAULT_SERIES_LENGTH = 16;

    public static final int DEFAULT_TOKEN_LENGTH = 16;

    private int seriesLength = DEFAULT_SERIES_LENGTH;

    private int tokenLength = DEFAULT_TOKEN_LENGTH;

    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,HttpServletResponse response) {
   
     
        // 判断cookieTokens的长度是否为2,如果不是说明格式错误,抛出异常
        if (cookieTokens.length != 2) {
   
     
            throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"+ Arrays.asList(cookieTokens) + "'");
        }
        String presentedSeries = cookieTokens[0];
        String presentedToken = cookieTokens[1];
        
        // 从内存中根据presentedSeries获取PersistentRememberMeToken
        PersistentRememberMeToken token	
            			= this.tokenRepository.getTokenForSeries(presentedSeries);
        if (token == null) {
   
     
            // No series match, so we can't authenticate using this cookie
            throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
        }
        // 判断用户输入的presentedToken和内存的tokenValue是否相同,不同则抛出异常清除token存储信息
        if (!presentedToken.equals(token.getTokenValue())) {
   
     
            // Token doesn't match series value. 
            // Delete all logins for this user and throw an exception to warn them.
            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."));
        }
        
        // 判断token是否过期,过期则抛出异常
        if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
   
     
            throw new RememberMeAuthenticationException("Remember-me login has expired");
        }
        // Token also matches, so login is valid. 
        // Update the token value, keeping the *same* series number.
        // 生成一个信息的tokenValue,并构造PersistentRememberMeToken
        PersistentRememberMeToken newToken 
            	= new PersistentRememberMeToken(token.getUsername(), 
                                                token.getSeries(),
                                                generateTokenData(),
                                                new Date());
        try {
   
     
            // 更新内存中存储的PersistentRememberMeToken
            this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
            // 将cookie添加到响应对象response中
            addCookie(newToken, request, response);
        }
        catch (Exception ex) {
   
     
            this.logger.error("Failed to update token: ", ex);
            throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
        }
        // 返回登录成功的用户详情信息
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

    protected String generateTokenData() {
   
     
        byte[] newToken = new byte[this.tokenLength];
        this.random.nextBytes(newToken);
        return new String(Base64.getEncoder().encode(newToken));
    }

    private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
   
     
        setCookie(new String[] {
   
      token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request,
                  response);
    }
}

  • 不同于 TokonBasedRemornberMeServices 中的 processAutologinCookie 方法,这里cookieTokens 数组的⻓度为2,第一项是series,第二项是 token。
  • 从cookieTokens数组中分到提取出 series 和 token. 然后根据 series 去内存中查询出一个 PersistentRememberMeToken对象。如果查询出来的对象为null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的 token 和从cookieTokens 中解析出来的token不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。
  • 根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
  • 生成一个新的 PersistentRememberMeToken 对象,用户名和series 不变,token 重新生成,date 也使用当前时间。newToken 生成后,根据 series 去修改内存中的token 和 date(即每次自动登录后都会产生新的 token 和 date)
  • 调用 addCookie 方法添加 Cookie, 在addCookie 方法中,会调用到我们前面所说的setCookie 方法,但是要注意第一个数组参数中只有两项:series 和 token(即返回到前端的令牌是通过对 series 和 token 进行 Base64 编码得到的)
  • 最后将根据用户名查询用户对象并返回。

3. 使用内存中令牌实现记住我功能

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User
                .withUsername("root")
                .password("{noop}123")
                .roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                // 开启rememberme功能
                .rememberMe()
                // 自定义 rememberMeServices
                .rememberMeServices(rememberMeServices())
                .and()
                .csrf().disable();
    }
    
    @Bean
    public RememberMeServices rememberMeServices(){
   
     
        // 参数1:自定义一个令牌key,默认为uuid
        // 参数2:认证数据源
        // 参数3:令牌存储方式
        return new PersistentTokenBasedRememberMeServices(
                "key", 
                userDetailsService(), 
                new InMemoryTokenRepositoryImpl()); 
    }
}

4. 持久化令牌

①引入依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

②SpringBoot配置文件:

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.hh.entity

logging.level.com.hh=debug
server.servlet.session.timeout=1

③配置持久化令牌:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Autowired
    private DataSource dataSource;

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User
                .withUsername("root")
                .password("{noop}123")
                .roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                // 开启rememberme功能
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .and()
                .csrf().disable();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
   
     
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}

启动项目并访问登录接口:后台自动生成数据库表
 

7. 传统Web开发自定义记住我功能

①登录页面 login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">
    用户名 : <input name="uname" type="text"> <br>
    密码: <input name="passwd" type="text"> <br>
    <!--value 可选值默认为: true  yes  on  1 都可以-->
    记住我: <input name="remember-me" type="checkbox" value="true"> <br>
    <input type="submit" value="登录">
</form>
<h3>
    <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h3>
</body>
</html>

@Controller
public class LoginController {
   
     
    @GetMapping("/login.html")
    public String login(){
   
     
        return "login";
    }
}

②登录成功跳转的 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<h1>欢迎进入我的主页</h1>
</body>
</html>

@Controller
public class IndexController {
   
     
    @RequestMapping("/index.html")
    public String index() {
   
     
        System.out.println("hello index");
        return "index";
    }
}

③配置SpringSecurity:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Autowired
    private DataSource dataSource;

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User
                .withUsername("root")
                .password("{noop}123")
                .roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .mvcMatchers("/login.html").permitAll()
                .mvcMatchers("/index.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .successForwardUrl("/index.html")
                .failureUrl("/login.html")
                .and()
                // 开启rememberMe功能
                .rememberMe()
                .rememberMeParameter("remember-me")
                .and()
                .csrf().disable();
    }
}

 

8. 前后端分离开发自定义记住我功能

①自定义过滤器替换 UsernamePasswordAuthenticationFilter

/**
 * 自定义前后端分离认证 Filter
 */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
   
     

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   
     
        System.out.println("========================================");
        //1.判断是否是 post 方式请求
        if (!request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //2.判断是否是 json 格式请求类型
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
   
     
            //3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx","remember-me":true}
            try {
   
     
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
                if (!ObjectUtils.isEmpty(rememberValue)) {
   
     
                    request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
                }
                System.out.println("用户名: " + username + " 密码: " + password + " 是否记住我: " + rememberValue);
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
   
     
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

②自定义 MyPersistentTokenBasedRememberMeServices

/**
 * 自定义记住我 services 实现类
 */
public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
   
     
    public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
   
     
        super(key, userDetailsService, tokenRepository);
    }
    /**
     * 自定义前后端分离获取 remember-me 方式
     *
     * @param request
     * @param parameter
     * @return
     */
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
   
     
        String paramValue = request.getAttribute(parameter).toString();
        if (paramValue != null) {
   
     
            if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                    || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
   
     
                return true;
            }
        }
        return false;
    }
}

③SpringSecurity配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
     
    @Bean
    public UserDetailsService userDetailsService() {
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
        auth.userDetailsService(userDetailsService());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
   
     
        return super.authenticationManagerBean();
    }

    //自定义 filter 交给工厂管理
    @Bean
    public LoginFilter loginFilter() throws Exception {
   
     
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/doLogin");//指定认证 url
        loginFilter.setUsernameParameter("uname");//指定接收json 用户名 key
        loginFilter.setPasswordParameter("passwd");//指定接收 json 密码 key
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setRememberMeServices(rememberMeServices()); //设置认证成功时使用自定义rememberMeService
        //认证成功处理
        loginFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
   
     
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录成功");
            result.put("用户信息", authentication.getPrincipal());
            resp.setContentType("application/json;charset=UTF-8");
            resp.setStatus(HttpStatus.OK.value());
            String s = new ObjectMapper().writeValueAsString(result);
            resp.getWriter().println(s);
        });
        //认证失败处理
        loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
   
     
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录失败: " + ex.getMessage());
            resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            resp.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            resp.getWriter().println(s);
        });
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        http.authorizeHttpRequests()
                .anyRequest().authenticated()//所有请求必须认证
                .and()
                .formLogin()
                .and()
                .rememberMe() //开启记住我功能  cookie 进行实现  1.认证成功保存记住我 cookie 到客户端   2.只有 cookie 写入客户端成功才能实现自动登录功能
                .rememberMeServices(rememberMeServices())  //设置自动登录使用哪个 rememberMeServices
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, ex) -> {
   
     
                    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                    resp.getWriter().println("请认证之后再去处理!");
                })
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                        new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                ))
                .logoutSuccessHandler((req, resp, auth) -> {
   
     
                    Map<String, Object> result = new HashMap<String, Object>();
                    result.put("msg", "注销成功");
                    result.put("用户信息", auth.getPrincipal());
                    resp.setContentType("application/json;charset=UTF-8");
                    resp.setStatus(HttpStatus.OK.value());
                    String s = new ObjectMapper().writeValueAsString(result);
                    resp.getWriter().println(s);
                })
                .and()
                .csrf().disable();
        // at: 用来某个 filter 替换过滤器链中哪个 filter
        // before: 放在过滤器链中哪个 filter 之前
        // after: 放在过滤器链中那个 filter 之后
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
    @Bean
    public RememberMeServices rememberMeServices() {
   
     
        return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());
    }
}

④测试:

@RestController
public class TestController {
   
     

    @GetMapping("/test")
    public String test() {
   
     
        System.out.println("test ....");
        return "test ok!";
    }
}