Spring Security 实现“记住我”功能及原理解析

网友投稿 510 2023-06-02

Spring Security 实现“记住我”功能及原理解析

这章继续扩展功能,来一个“记住我”的功能实现,就是说用户在登录一次以后,系统会记住这个用户一段时间,这段时间内用户不需要重新登录就可以使用系统。

记住我功能基本原理

原理说明

用户登录发送认证请求的时候会被UsernamePasswordAuthenticationFilter认证拦截,认证成功以后会调用一个RememberMeService服务,服务里面有一个TokenRepository,这个服务会生成一个Token,然后将Token写入到浏览器的Cookie同时会使用TokenRepository把生成的Token写到数据库里面,因为这个动作是在认证成功以后做的,所以在Token写入数据库的时候会把用户名同时写入数据库。

假如浏览器关了重新访问系统,用户不需要再次登录就可以访问,这个时候请求在过滤器链上会经过RememberMeAuthenticationFilter,这个过滤器的作用是读取Cookie中的Token交给RemeberMeService,RemeberMeService会用TokenRepository到数据库里去查这个Token在数据库里有没有记录,如果有记录就会把用户名取出来,取出来以后会进行各种校验然后生成新Token再调用之前的UserDetailService,去获取用户的信息,然后把用户信息放到SecurityContext里面,到这里就把用户给登录上了。

图解说明

RememberMeAuthenticationFilter位于过滤器链的哪一环?

图解

首先其他认证过滤器会先进行认证,当其他过滤器都无法认证时,RememberMeAuthenticationFilter会尝试去做认证。

记住我功能具体实现

前端页面

登录的时候加上一行记住我的勾选按钮,这里要注意,name一定要是remember-me,下面源码部分会提到。

后台

首先配置TokenRepositoryBean

/**

* 记住我功能的Token存取器配置

*

* @return

*/

@Bean

public PersistentTokenRepository persistentTokenRepository() {

JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();

tokenRepository.setDataSource(dataSource);

// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了

tokenRepository.setCreateTableOnStartup(true);

return tokenRepository;

}

然后需要在 configure 配置方法那边进行记住我功能所有组件的配置

protected v-oid configure(HttpSecurity http) throws Exception {

ValidateCodeFilter validateCodeFilter = new V-alidateCodeFilter();

http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)

.formLogin()

.loginPage("/authentication/require")

.loginProcessingUrl("/authentication/form")

.successHandler(meicloudAuthenticationSuccessHandler)

.failureHandler(meicloudAuthenticationFailureHandler)

// 配置记住我功能

.and()

.rememberMe()

// 配置TokenRepository

.tokenRepository(persistentTokenRepository())

// 配置Token过期时间

.tokenValiditySeconds(3600)

// 最终拿到用户名之后,使用UserDetailsService去做登录

.userDetailsService(userDetailsService)

.and()

.authorizeRequests()

.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/image").permitAll()

.anyRequest()

.authenticated()

.and()

.csrf().disable();

}

记住我功能Spring Security源码解析

登录之前“记住我”源码流程

在认证成功之后,会调用successfulAuthentication方法(这些第五章源码部分已经学习过),在将认证信息保存到Context后,RememberMeServices就会调用它的loginSuccess方法。

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticaeKGgnDYOletion authResult) throws IOException, ServletException {

if (this.logger.isDebugEnabled()) {

this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);

}

SecurityContextHolder.getContext().setAuthentication(authResult);

this.rememberMeServices.loginSuccess(request, response, authResult);

if (this.eventPublisher != null) {

this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));

}

this.successHandler.onAuthenticationSuccess(request, response, authResult);

}

loginSuccess方法里面会先检查请求中是否有name为remember-me的参数,有才进行下一步。

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

// this.parameter = "remember-me"

if (!this.rememberMeRequested(request, this.parameter)) {

this.logger.debug("Remember-me login not requested.");

} else {

this.onLoginSuccess(request, response, successfulAuthentication);

}

}

再进入onLoginSuccess方法,里面主要就是进行写库和写Cookie的操作。

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {

String username = successfulAuthentication.getName();

this.logger.debug("Creating new persistent login for user " + username);

// 生成Token

PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

try {

// 将Token和userName插入数据库

this.tokenRepository.createNewToken(persistentToken);

// 将Token写到Cookie中

this.addCookie(persistentToken, request, response);

} catch (Exception var7) {

this.logger.error("Failed to save persistent token ", var7);

}

}

登录之后“记住我”源码流程

首先会进入RememberMeAuthenticationFilter,会先判断前面的过滤器是否进行过认证(Context中是否有认证信息),未进行过认证的话会调用RememberMeServices的autoLogin方法。

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 = this.rememberMeServices.autoLogin(request, response);

if (rememberMeAuth != null) {

try {

rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);

SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

this.onSuccessfulAuthentication(request, response, rememberMeAuth);

if (this.logger.isDebugEnabled()) {

this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");

}

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 var8) {

if (this.logger.isDebugEnabled()) {

this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);

}

this.rememberMeServices.loginFail(request, response);

this.onUnsuccessfulAuthentication(request, response, var8);

}

}

chain.doFilter(request, response);

} else {

if (this.logger.isDebugEnabled()) {

this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");

}

chain.doFilter(request, response);

}

}

autoLogin方法里面,主要调用this.processAutoLoginCookie(cookieTokens, request, response)这个方法获取数据库中的用户信息,其步骤是:

解析前端传来的Cookie,里面包含了Token和seriesId,它会使用seriesId查找数据库的Token

检查Cookie中的Token和数据库查出来的Token是否一样

一样的话再检查数据库中的Token是否已过期

如果以上都符合的话,会使用旧的用户名和series重新new一个Token,这时过期时间也重新刷新

然后将新的Token保存回数据库,同时添加回Cookie中

最后再调用UserDetailsService的loadUserByUsername方法返回UserDetails

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) + "'");

} else {

String presentedSeries = cookieTokens[0];

String presentedToken = cookieTokens[1];

PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);

if (token == null) {

throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);

} else if (!presentedToken.equals(token.getTokenValue())) {

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."));

} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {

throw new RememberMeAuthenticationException("Remember-me login has expired");

} else {

if (this.logger.isDebugEnabled()) {

this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");

}

PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

try {

this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());

this.addCookie(newToken, request, response);

} cat-ch (Exception var9) {

this.logger.error("Failed to update token: ", var9);

throw new RememberMeAuthenticationException("Autologin failed due to data access problem");

}

return this.getUserDetailsService().loadUserByUsername(token.getUsername());

}

}

}

回到RememberMeAuthenticationFilter,在调用了autoLogin方法之后得到了rememberMeAuth,然后再对其进行一个认证,认证成功之后保存到SecurityContext中,至此整个RememberMe自动登录流程源码结束。

相关阅读:

Spring Security实现图形验证码登录

Spring Security实现短信验证码登录

总结

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail- 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:SpringBoot连接Redis2种模式解析
下一篇:Spring事务管理配置文件问题排查
相关文章

 发表评论

暂时没有评论,来抢沙发吧~