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,下面源码部分会提到。

<tr>
				<td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
			</tr>

后台

首先配置TokenRepositoryBean

/**
	 * 记住我功能的Token存取器配置
	 *
	 * @return
	 */
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
		tokenRepository.setDataSource(dataSource);
		// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
		tokenRepository.setCreateTableOnStartup(true);
		return tokenRepository;
	}

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

protected void configure(HttpSecurity http) throws Exception {
		ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
		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, Authentication 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);
  } catch (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实现短信验证码登录

总结

到此这篇关于Spring Security 实现“记住我”功能及原理解析的文章就介绍到这了,更多相关spring security记住我内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Security实现两周内自动登录"记住我"功能

    本文是Spring Security系列中的一篇.在上一篇文章中,我们通过实现UserDetailsService和UserDetails接口,实现了动态的从数据库加载用户.角色.权限相关信息,从而实现了登录及授权相关的功能.这一节就在此基础上新增,登录过程中经常使用的"记住我"功能,也就是我们经常会在各种网站登陆时见到的"两周内免登录","三天内免登录"的功能.该功能的作用就是:当我们登录成功之后,一定的周期内当我们再次访问该网站,不需要重新登

  • Spring security实现记住我下次自动登录功能过程详解

    一.原理分析 第一次登陆时,如果用户勾选了readme选项,登陆成功后springsecurity会生成一个cookie返回给浏览器端,浏览器下次访问时如果携带了这个cookie,springsecurity就会放行这次访问. 二.实现方式 2.1 简单实现方式 (1) 在springsecurity的配置文件中,http节点下增加一个remember-me配置 <security:http auto-config="true" use-expressions="fal

  • Spring Security 构建rest服务实现rememberme 记住我功能

    Spring security记住我基本原理: 登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeService,会生成一个token,将token写入到浏览器cookie,同时RememberMeService里边还有个TokenRepository,将token和用户信息写入到数据库中.这样当用户再次访问系统,访问某一个接口时,会经过一个RememberMeAuthenticationFilt

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

    这章继续扩展功能,来一个"记住我"的功能实现,就是说用户在登录一次以后,系统会记住这个用户一段时间,这段时间内用户不需要重新登录就可以使用系统. 记住我功能基本原理 原理说明 用户登录发送认证请求的时候会被UsernamePasswordAuthenticationFilter认证拦截,认证成功以后会调用一个RememberMeService服务,服务里面有一个TokenRepository,这个服务会生成一个Token,然后将Token写入到浏览器的Cookie同时会使用TokenR

  • Spring Security 表单登录功能的实现方法

    1.简介 本文将重点介绍使用 Spring Security 登录. 本文将构建在之前简单的 Spring MVC示例 之上,因为这是设置Web应用程序和登录机制的必不可少的. 2. Maven 依赖 要将Maven依赖项添加到项目中,请参阅Spring Security with Maven 一文. 标准的 spring-security-web 和 spring-security-config 都是必需的. 3. Spring Security Java配置 我们首先创建一个扩展 WebSe

  • Spring Security实现验证码登录功能

    这篇文章主要介绍了Spring Security实现验证码登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在spring security实现登录注销功能的基础上进行开发. 1.添加生成验证码的控制器. (1).生成验证码 /** * 引入 Security 配置属性类 */ @Autowired private SecurityProperties securityProperties; @Override public ImageC

  • 基于spring security实现登录注销功能过程解析

    这篇文章主要介绍了基于spring security实现登录注销功能过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.引入maven依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependenc

  • SpringBoot使用Spring Security实现登录注销功能

    1.首先看下我的项目结构 我们逐个讲解 /** * 用户登录配置类 * @author Administrator * */ public class AdminUserDateils implements UserDetails { private static final long serialVersionUID = -1546619839676530441L; private transient YCAdmin yCAdmin; public AdminUserDateils() { }

  • Spring Security实现自动登陆功能示例

    当我们在登录像QQ邮箱这种大多数的网站,往往在登录按键上会有下次自动登录这个选项,勾选后登录成功,在一段时间内,即便退出浏览器或者服务器重启,再次访问不需要用户输入账号密码进行登录,这也解决了用户每次输入账号密码的麻烦. 接下来实现自动登陆. applicatio.properties配置用户名密码 spring.security.user.name=java spring.security.user.password=java controller层实现 @RestController pub

  • SPRING FRAMEWORK BEAN作用域和生命周期原理解析

    这篇文章主要介绍了SPRING FRAMEWORK BEAN作用域和生命周期原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Spring beand的作用域 设置为singleton时,相当于一个类只能有一个实例,当再次申请时,返回的是同一个实例 可以看到两个bean实例的hashcode值是一样的,说明在此申请到的是同一个实例 将bean的作用域设置为prototype时,再次运行,可以看到,申请到的是两个不同bean实例 目前只学习

  • Spring Boot ActiveMQ发布/订阅消息模式原理解析

    本文在<Spring Boot基于Active MQ实现整合JMS>的基础上,介绍如何使用ActiveMQ的发布/订阅消息模式.发布/订阅消息模式是消息发送者发送消息到主题(topic),而多个消息接收者监听这个主题:其中,消息发送者和接收者分别叫做发布者(publisher)和订阅者(subscriber),对于发布者来说,它和所有的订阅者就构成了一个1对多的关系.这种关系如下图所示: 发布/订阅模式的工作示意图 消息生产者将消息(发布)到topic中,可以同时有多个消息消费者(订阅)消费该

  • spring security实现下次自动登录功能过程解析

    这篇文章主要介绍了spring security实现记住我下次自动登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.原理分析 第一次登陆时,如果用户勾选了readme选项,登陆成功后springsecurity会生成一个cookie返回给浏览器端,浏览器下次访问时如果携带了这个cookie,springsecurity就会放行这次访问. 二.实现方式 2.1 简单实现方式 (1) 在springsecurity的配置文件中,http节

随机推荐