Spring Security 6 记住我功能实现指南
Spring Security 6 提供了强大的"记住我"功能实现,允许用户在会话关闭后仍保持登录状态。以下是完整的实现方案和代码示例:
核心实现方案
1. 基于持久化令牌的实现(推荐)
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource; // 数据源
@Autowired
private UserDetailsService userDetailsService;
// 配置持久化令牌存储
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepo = new JdbcTokenRepositoryImpl();
tokenRepo.setDataSource(dataSource);
// 启动时创建表(仅第一次使用)
// tokenRepo.setCreateTableOnStartup(true);
return tokenRepo;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(1209600) // 14天有效期
.rememberMeParameter("remember-me") // 表单参数名
.rememberMeCookieName("remember-me-cookie") // Cookie名称
.useSecureCookie(true) // 仅HTTPS传输
.key("uniqueAndSecretKey") // 加密密钥
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID", "remember-me-cookie") // 删除Cookie
);
return http.build();
}
}
2. 基于签名Cookie的实现(简单)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("uniqueAndSecretKey") // 必须设置
.tokenValiditySeconds(1209600) // 14天
.rememberMeParameter("remember-me")
.userDetailsService(userDetailsService)
);
return http.build();
}
数据库表结构
Spring Security 需要以下表存储持久化令牌:
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
前端登录表单
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form th:action="@{/login}" method="post">
<div>
<label>Username:</label>
<input type="text" name="username" required>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" required>
</div>
<div>
<label>
<input type="checkbox" name="remember-me"/> Remember me
</label>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>
自定义记住我服务
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
public class CustomRememberMeServices extends PersistentTokenBasedRememberMeServices {
public CustomRememberMeServices(String key,
UserDetailsService userDetailsService,
PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
@Override
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication successfulAuthentication) {
// 自定义登录成功处理
String username = successfulAuthentication.getName();
logger.info("Remember-me login successful for user: " + username);
super.onLoginSuccess(request, response, successfulAuthentication);
}
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request,
HttpServletResponse response)
throws RememberMeAuthenticationException, UsernameNotFoundException {
try {
return super.processAutoLoginCookie(cookieTokens, request, response);
} catch (RememberMeAuthenticationException e) {
// 自定义异常处理
logger.error("Remember-me authentication failed: " + e.getMessage());
throw e;
}
}
}
在配置中使用自定义服务:
@Bean
public RememberMeServices rememberMeServices() {
CustomRememberMeServices rememberMeServices = new CustomRememberMeServices(
"uniqueAndSecretKey",
userDetailsService,
persistentTokenRepository()
);
rememberMeServices.setTokenValiditySeconds(1209600);
rememberMeServices.setParameter("remember-me");
rememberMeServices.setCookieName("remember-me-cookie");
return rememberMeServices;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.rememberMeServices(rememberMeServices())
);
return http.build();
}
记住我功能的高级配置
1. 多设备管理
@Service
public class RememberMeTokenService {
@Autowired
private PersistentTokenRepository tokenRepository;
public List<PersistentRememberMeToken> getUserTokens(String username) {
return tokenRepository.findByUsername(username);
}
public void revokeToken(String series) {
tokenRepository.removeToken(series);
}
public void revokeAllUserTokens(String username) {
tokenRepository.removeUserTokens(username);
}
}
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private RememberMeTokenService tokenService;
@GetMapping("/sessions")
public ResponseEntity<List<SessionInfo>> getUserSessions(
@AuthenticationPrincipal UserDetails user) {
List<PersistentRememberMeToken> tokens = tokenService.getUserTokens(user.getUsername());
List<SessionInfo> sessions = tokens.stream().map(token ->
new SessionInfo(
token.getSeries(),
token.getTokenValue(),
token.getDate()
)
).collect(Collectors.toList());
return ResponseEntity.ok(sessions);
}
@DeleteMapping("/sessions/{series}")
public ResponseEntity<?> revokeSession(@PathVariable String series) {
tokenService.revokeToken(series);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/sessions")
public ResponseEntity<?> revokeAllSessions(@AuthenticationPrincipal UserDetails user) {
tokenService.revokeAllUserTokens(user.getUsername());
return ResponseEntity.noContent().build();
}
public static class SessionInfo {
private final String series;
private final String token;
private final Date lastUsed;
// 构造函数、getters
}
}
2. 安全事件监听
@Component
public class RememberMeEventListener {
private static final Logger logger = LoggerFactory.getLogger(RememberMeEventListener.class);
@EventListener
public void handleInteractiveAuthenticationSuccess(
InteractiveAuthenticationSuccessEvent event) {
if (event.getAuthentication() instanceof RememberMeAuthenticationToken) {
String username = event.getAuthentication().getName();
logger.info("Remember-me login successful for user: " + username);
// 发送通知、记录审计日志等
}
}
@EventListener
public void handleAuthenticationFailure(
AbstractAuthenticationFailureEvent event) {
if (event.getException() instanceof RememberMeAuthenticationException) {
String sourceIp = ((WebAuthenticationDetails)
event.getAuthentication().getDetails()).getRemoteAddress();
logger.warn("Remember-me authentication failed from IP: " + sourceIp);
// 安全告警、限制访问等
}
}
}
安全最佳实践
-
密钥管理:
- 使用强密钥(至少32字符)
- 定期轮换密钥(会导致所有记住我令牌失效)
- 从安全配置存储获取密钥,而非硬编码
-
Cookie安全:
.rememberMe(remember -> remember .rememberMeCookieName("rm") .useSecureCookie(true) // 仅HTTPS .cookieDomain("yourdomain.com") .cookieHttpOnly(true) // 防止XSS .cookiePath("/") )
-
会话管理:
- 设置合理的令牌有效期(14-30天)
- 提供用户管理活动会话的能力
- 敏感操作前要求重新认证
-
令牌安全:
- 使用持久化令牌而非签名Cookie
- 每次认证后更新令牌
- 监控异常登录模式
完整示例:用户界面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Account Security</title>
<style>
.session-card { border: 1px solid #ddd; padding: 15px; margin-bottom: 10px; }
.current-session { background-color: #e6f7ff; }
</style>
</head>
<body>
<h1>Active Sessions</h1>
<div th:each="session : ${sessions}"
th:class="${session.current ? 'session-card current-session' : 'session-card'}">
<h3 th:text="${session.browser} + ' on ' + ${session.os}">Browser on OS</h3>
<p>Location: <span th:text="${session.location}">Location</span></p>
<p>IP Address: <span th:text="${session.ipAddress}">IP</span></p>
<p>Last Accessed: <span th:text="${session.lastAccess}">Date</span></p>
<div th:if="${!session.current}">
<button th:onclick="'revokeSession(\'' + ${session.series} + '\')'">Revoke</button>
</div>
</div>
<div th:if="${#lists.size(sessions) > 1}">
<button onclick="revokeAllSessions()">Revoke All Other Sessions</button>
</div>
<script>
function revokeSession(series) {
fetch('/account/sessions/' + series, { method: 'DELETE' })
.then(response => {
if (response.ok) {
alert('Session revoked');
location.reload();
}
});
}
function revokeAllSessions() {
fetch('/account/sessions', { method: 'DELETE' })
.then(response => {
if (response.ok) {
alert('All other sessions revoked');
location.reload();
}
});
}
</script>
</body>
</html>
配置参数说明
配置项 | 默认值 | 说明 |
---|---|---|
tokenValiditySeconds | 1209600 (14天) | 记住我令牌有效期 |
alwaysRemember | false | 是否总是记住 |
useSecureCookie | 跟随请求安全属性 | 是否仅HTTPS发送Cookie |
rememberMeParameter | “remember-me” | 前端复选框名称 |
rememberMeCookieName | “remember-me” | Cookie名称 |
key | 随机UUID | 签名密钥(必须设置) |
tokenRepository | null | 持久化令牌存储(推荐使用) |
故障排除
-
记住我功能不生效:
- 检查前端表单参数名是否匹配
- 确认密钥配置一致
- 验证数据库连接正常(持久化令牌)
- 检查Cookie是否被浏览器阻止
-
登录后立即失效:
- 确保服务器时间正确
- 验证令牌有效期设置
- 检查是否有多个服务实例共享密钥
-
安全警告:
- 确保使用HTTPS
- 设置HttpOnly和Secure Cookie标志
- 定期轮换加密密钥
Spring Security 6 的记住我功能提供了灵活且安全的实现方案。持久化令牌方案比简单的签名Cookie更安全,推荐在生产环境中使用。同时结合多设备管理和安全监控,可以构建更加安全的认证系统。