SpringSecurity学习日记(4):记住我(remember-me)功能

it2025-08-10  9

图片摘自此处,在此感谢

在用户认证成功后,会调用AbstractAuthenticationProcessingFilter类的successfulAuthentication()方法

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); // 记住我功能 rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }

在这个方法中rememberMeServices.loginSuccess(request, response, authResult)便跟记住我功能有关。 RememberMeServices接口提供了记住我功能。具体功能由其实现类实现。我们再回到上面的loginSuccess()方法。它由AbstractRememberMeServices实现。

public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!rememberMeRequested(request, parameter)) { logger.debug("Remember-me login not requested."); return; } onLoginSuccess(request, response, successfulAuthentication); }

首先我们要通过rememberMeRequested(request, parameter)这个方法判断我们是否选择了记住我功能。

protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { if (alwaysRemember) { return true; } // parameter值为 "remember-me" // 获取前台传入的 "remember-me" 的值 String paramValue = request.getParameter(parameter); // 非空判断 if (paramValue != null) { // 获取到的值为 true,on,yes,1 中的任意一种,即表示开启记住我功能 if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) { return true; } } if (logger.isDebugEnabled()) { logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')"); } return false; } // parameter 的值为 "remember-me" private String parameter = DEFAULT_PARAMETER; public static final String DEFAULT_PARAMETER = "remember-me";

要实现记住我功能,从这个方法中我们可以得出两点:

前台标签中的 name 属性的值必须为 “remember-me”(当然我们也可以自定义,下面会讲到)前台标签的 value 属性的值必须为 true, on, yes, 1 中的一种

如果满足上述条件,继续调用onLoginSuccess()方法,该方法具体由AbstractRememberMeServices 的子类PersistentTokenBasedRememberMeServices实现

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { // 在数据库中保存cookie信息 tokenRepository.createNewToken(persistentToken); // 在浏览器中保存cookie信息 addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }

该方法做了两件事,一是在数据库中保存cookie信息,二是在浏览器中保存cookie信息。 tokenRepository.createNewToken(persistentToken)添加cookie信息到数据库中,具体方法由子类JdbcTokenRepositoryImpl实现

public void createNewToken(PersistentRememberMeToken token) { // 获取Spring中的JdbcTemplate执行update()操作 getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()); }

我们看一下这个insertTokenSql

public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";

所以这里就是调用JdbcTemplate类执行update()操作向persistent_logins 这个表中插入你的用户信息以及cookie信息。而当你下次登录时,又会调用查询语句比对数据库中的cookie信息与浏览器中的cookie信息是否一致,来完成自动登录。到这里我们已经完成了cookie的保存操作。再来看一下自动登录时怎么实现的。 我们回到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 = rememberMeServices.autoLogin(request, response); ... } }

首先会先判断前面的过滤器是否进行过认证(SecurityContext中是否有认证信息,认证后的信息会保存在SecurityContext中),未进行过认证的话会调用RememberMeServices的autoLogin()方法。该方法具体由子类AbstractRememberMeServices实现

public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { // 从浏览器的请求域中获取cookie信息 String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; } logger.debug("Remember-me cookie detected"); if (rememberMeCookie.length() == 0) { logger.debug("Cookie was empty"); cancelCookie(request, response); return null; } UserDetails user = null; try { // 获取解析后的cookie值 String[] cookieTokens = decodeCookie(rememberMeCookie); // 进行自动登录验证 user = processAutoLoginCookie(cookieTokens, request, response); // 检查user的用户信息是否可用 userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted"); return createSuccessfulAuthentication(request, user); } ... }

在该方法中又调用了 processAutoLoginCookie()方法比对浏览器中的cookie信息和数据库中保存的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) + "'"); } final String presentedSeries = cookieTokens[0]; final String presentedToken = cookieTokens[1]; // 获取数据库中保存的cookie信息 PersistentRememberMeToken token = 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); } // We have a match for this user/series combination if (!presentedToken.equals(token.getTokenValue())) { // Token doesn't match series value. Delete all logins for this user and throw // an exception to warn them. tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException( messages.getMessage( "PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); } if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System .currentTimeMillis()) { throw new RememberMeAuthenticationException("Remember-me login has expired"); } if (logger.isDebugEnabled()) { logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'"); } PersistentRememberMeToken newToken = new PersistentRememberMeToken( token.getUsername(), token.getSeries(), generateTokenData(), new Date()); try { tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); addCookie(newToken, request, response); } catch (Exception e) { logger.error("Failed to update token: ", e); throw new RememberMeAuthenticationException( "Autologin failed due to data access problem"); } // 登录用户 return getUserDetailsService().loadUserByUsername(token.getUsername()); }

tokenRepository.getTokenForSeries(presentedSeries)用于获取数据库中保存的cookie信息。如果比对失败则会抛出异常。比对成功,会调用tokenRepository.updateToken()方法更新cookie信息,用于下一次比对。最后调用getUserDetailsService().loadUserByUsername(token.getUsername());进行用户信息验证,登录用户。这里便实现了用户的自动登录。

再说一下processAutoLoginCookie()方法中的具体步骤

解析前端传来的Cookie,里面包含了Token和seriesId,它会使用seriesId查找数据库的Token检查Cookie中的Token和数据库查出来的Token是否相同相同的话再检查数据库中的Token是否已过期如果以上都符合的话,会使用旧的用户名和series重新new一个Token,这时过期时间也重新刷新然后将新的Token保存回数据库,同时添加回Cookie中最后再调用UserDetailsService的loadUserByUsername()方法返回UserDetails对象完成登录

代码实现 persistent_logins表(这张表也可以由springsecurity为我们创建,下面有讲到)

CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8

pom.xml

<dependencies> <!-- SpringSecurity --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <!-- mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> </dependencies>

application.yml

server: port: 8080 spring: datasource: # 设置数据库连接池类型 type: com.alibaba.druid.pool.DruidDataSource # 设置驱动类(因为我用的是 mysql-connector 8,所以要加 cj) driver-class-name: com.mysql.cj.jdbc.Driver # 记得加 serverTimezone url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai username: root password: 990515

UserService

@Service public class UserService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("admin"); return new User("guest", new BCryptPasswordEncoder().encode("123"), authorities); } }

UserController

@RestController @RequestMapping("/user") public class UserController { @GetMapping("/login") public String login() { return "登录成功"; } }

SecurityConfig

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; /** * 注入加密器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 注入数据源 */ @Autowired private DataSource dataSource; /** * 注入 PersistentTokenRepository */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository= new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 可以为我们自动创建表 persistent_logins (若数据库已存在该表,执行该语句则会报错) // tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } /** * 认证用户 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * 拦截http请求 */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 放行请求 .antMatchers("/", "/login.html").permitAll() .anyRequest().authenticated() .and() // 设置登录页面 .formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .and() // 开启记住我功能 .rememberMe() // (可选)设置前端传递过来的 remember-me 功能的属性名 // 默认的属性名为 remember-me .rememberMeParameter("my-remember-me") // (可选)设置 remember-me 功能对应的 cookie 名 // 默认的 cookie 名为 remember-me .rememberMeCookieName("my-remember-me-cookie") // 设置 PersistentTokenRepository (将cookie信息存储到数据库中) .tokenRepository(persistentTokenRepository()) // 设置 Cookie 的有效期为1小时 .tokenValiditySeconds(60*60) // 设置 UserDetailsService .userDetailsService(userDetailsService) .and() // 关闭csrf .csrf().disable(); } }

login.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录页面</title> </head> <body> <form action="/user/login" method="post"> 用户名: <input type="text" name="username"> <br/> 密码: <input type="password" name="password"> <br/> <input type="checkbox" name="my-remember-me"> 记住我 <br/> <input type="submit" value="登录"> </form> </body> </html>

最新回复(0)