RememberMe 实现
1286字约4分钟
2024-08-08
实现 RememberMe 功能
IndexController 登录成功返回当前用户信息
@Controller
public class IndexController {
/**
* 从 SecurityContextHolder 获取信息
* @return
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
@ResponseBody
public String index() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return JSON.toJSONString(authentication);
}
}
开启 rememberMe 功能,只需要配置下 .rememberMe()
即可
/**
* @ClassName SecurityConfig
* @Desciption Security 配置
* @Author MaRui
* @Date 2023/9/27 15:59
* @Version 1.0
*/
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
.and()
.csrf().disable();
return http.build();
}
}
验证一下,访问登录页面 http://127.0.0.1:8080,由于没有配置账号密码,使用默认的 user
,以及控制台生成的密码,并勾选 RememberMe

登录成功,返回了用户的认证信息

关闭浏览器,重新打开浏览器访问页面 http://127.0.0.1:8080,在 network
的请求中,我们可以看到携带了 cookie
信息完成了自动登录

RememberMe 实现原理分析
首先我们找切入点,每当引入一个新功能的时候,必定会引入一个 configurer , rememberMe 也一样,点击 SecurityConfig
配置类中 .rememberMe()
进入源码
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
public RememberMeConfigurer<HttpSecurity> rememberMe() throws Exception {
return getOrApply(new RememberMeConfigurer<>());
}
}
RememberMeConfigurer
中我们主要看 init
和 configure
方法
init
validateInput()
:校验入参,rememberMeCookieName
不等于默认值remember-me
则抛出IllegalArgumentException
异常getKey()
:获取配置的key
值,没有配置则随机生成。如果没有配置key
值,系统每次重新启动,页面上都需要重新登录getRememberMeServices()
:用来实现自动登录、处理登录成功和登录失败的逻辑,主要有两个继承PersistentTokenBasedRememberMeServices
:持久化令牌到数据库TokenBasedRememberMeServices
:默认的RememberMeServices
http.authenticationProvider(authenticationProvider)
:创建认证器并放入到 providerList 中initDefaultLoginFilter(http)
:自动生成带 RememberMe 选框的登录页面
public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
@Override
public void init(H http) throws Exception {
// 校验入参,其中 rememberMeCookieName 如果不等于默认的值 `remember-me` 就抛出 IllegalArgumentException 异常
validateInput();
// 获取 key 值,如果在 SecurityConfig 没有配置这个 key 值,则初始化时随机 this.key = UUID.randomUUID().toString()
// 如果没有配置 key 值,系统每次重新启动,页面上都需要重新登录
String key = getKey();
// getRememberMeServices,用来实现自动登录、处理登录成功和登录失败的逻辑,
// SecurityConfig 中不配置 PersistentTokenRepository 则默认使用 TokenBasedRememberMeServices 这个实现
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);
// 创建认证器并放入到 providerList 中
http.authenticationProvider(authenticationProvider);
// 自动生成带 RememberMe 选框的登录页面
initDefaultLoginFilter(http);
}
}
configure
new RememberMeAuthenticationFilter()
:创建了 RememberMeAuthenticationFilter 过滤器postProcess(rememberMeFilter)
:将过滤器注入 IOC 容器http.addFilter(rememberMeFilter)
:在 http 中加入 rememberMe 过滤器
public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
@Override
public void configure(H http) {
// 创建了 RememberMeAuthenticationFilter 过滤器
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class), this.rememberMeServices);
if (this.authenticationSuccessHandler != null) {
rememberMeFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
// 将过滤器注入 IOC 容器
rememberMeFilter = postProcess(rememberMeFilter);
// 在 http 中加入 rememberMe 过滤器
http.addFilter(rememberMeFilter);
}
}
看完了RememberMeConfigurer的两个核心方法之后,我们现在知道容器中已经有了 RememberMeAuthenticationFilter 和 RememberMeAuthenticationProvider 现在分别来看下
RememberMeAuthenticationFilter
调用autoLogin方法
从cookie提取用户身份,在调用loadUserByUsername获取用户详细信息
校验用户身份是否合法(根据不同的令牌存储方式进行不同校验)
如果校验通过,则返回Authentication(RememberMeAuthenticationToken)
authenticate
如果autoLogin成功,则和前面普通登录一样进行认证
因为这里生成的是RememberMeAuthenticationToken,则最终会被RememberMeAuthenticationProvider处理
如果认证返回则进行对应的响应处理
public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage
.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
chain.doFilter(request, response);
return;
}
// 调用 autoLogin 方法
// 1、从 cookie 提取用户身份,在调用 loadUserByUsername 获取用户详细信息
// 2、校验用户身份是否合法(根据不同的令牌存储方式进行不同校验)
// 3、如果校验通过,则返回 Authentication(RememberMeAuthenticationToken)
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// authenticate
// 如果autoLogin成功,则和前面普通登录一样进行认证
// 因为这里生成的是RememberMeAuthenticationToken,则最终会被RememberMeAuthenticationProvider处理
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// 如果认证返回则进行对应的响应处理
// Store to SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(rememberMeAuth);
SecurityContextHolder.setContext(context);
onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
this.securityContextRepository.saveContext(context, request, response);
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.logger.debug(LogMessage
.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
+ "rejected Authentication returned by RememberMeServices: '%s'; "
+ "invalidating remember-me token", rememberMeAuth),
ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
}
RememberMeAuthenticationProvider
这里的逻辑只做了一步,校验key的hashCode是否一致,如果一致,则校验通过
public class RememberMeAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return authentication;
}
}