Spring Security 6 【6-记住我功能】

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);
            // 安全告警、限制访问等
        }
    }
}

安全最佳实践

  1. 密钥管理

    • 使用强密钥(至少32字符)
    • 定期轮换密钥(会导致所有记住我令牌失效)
    • 从安全配置存储获取密钥,而非硬编码
  2. Cookie安全

    .rememberMe(remember -> remember
        .rememberMeCookieName("rm")
        .useSecureCookie(true) // 仅HTTPS
        .cookieDomain("yourdomain.com")
        .cookieHttpOnly(true) // 防止XSS
        .cookiePath("/")
    )
    
  3. 会话管理

    • 设置合理的令牌有效期(14-30天)
    • 提供用户管理活动会话的能力
    • 敏感操作前要求重新认证
  4. 令牌安全

    • 使用持久化令牌而非签名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>

配置参数说明

配置项默认值说明
tokenValiditySeconds1209600 (14天)记住我令牌有效期
alwaysRememberfalse是否总是记住
useSecureCookie跟随请求安全属性是否仅HTTPS发送Cookie
rememberMeParameter“remember-me”前端复选框名称
rememberMeCookieName“remember-me”Cookie名称
key随机UUID签名密钥(必须设置)
tokenRepositorynull持久化令牌存储(推荐使用)

故障排除

  1. 记住我功能不生效

    • 检查前端表单参数名是否匹配
    • 确认密钥配置一致
    • 验证数据库连接正常(持久化令牌)
    • 检查Cookie是否被浏览器阻止
  2. 登录后立即失效

    • 确保服务器时间正确
    • 验证令牌有效期设置
    • 检查是否有多个服务实例共享密钥
  3. 安全警告

    • 确保使用HTTPS
    • 设置HttpOnly和Secure Cookie标志
    • 定期轮换加密密钥

Spring Security 6 的记住我功能提供了灵活且安全的实现方案。持久化令牌方案比简单的签名Cookie更安全,推荐在生产环境中使用。同时结合多设备管理和安全监控,可以构建更加安全的认证系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值