SpringSecurity学习笔记三

基于用户名、密码的"记住我"功能

SpringBoot 2.2.0.RELEASE

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

记住我功能流程图
"记住我"功能的流程是:

  • 输入验证信息,如用户名、密码、验证码等
  • 选中 “记住我”,系统后台根据登陆成功的用户信息 生成token信息 放到cookie和数据库表中
  • 【系统重新启动或退出系统重新访问】,在cookie的过期时间内 重新登录时 都不需要输入用户名、密码,可以直接访问请求资源。

首先,需要明确的是"记住我"是SpringSecurity默认带的,过滤器和认证类分别是RememberMeAuthenticationFilterRememberMeAuthenticationProvider我们只需要配置即可

需要注意的是,使用记住我功能,前端页面必须要有一个name=remember-me的checkbox,
因为SpringSecurity中与"记住我"有关的功能,将remember-me作为参数名来进行业务处理,如AbstractRememberMeServices#rememberMeRequested()

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

讲到"记住我"功能,还要从之前的`UserNamePasswordAuthenticationFilter`说起,当执行了`attemptAuthentication`之后,会执行``successfulAuthentication(request, response, chain, authResult);``

有代码为证 AbstractAuthenticationProcessFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		//调用UserNamePasswordAuthenticationFilter方法
			authResult = attemptAuthentication(request, response);
		
		//此处省略一些代码
		
		successfulAuthentication(request, response, chain, authResult);
	}

着重看一下successfulAuthentication方法

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

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//将验证过后的信息 存到SecurityContextHolder中
	SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//调用成功处理器
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

会发现,该方法除了将认证信息放到SecurityContext以外,还调用了rememberMeServices.loginSuccess方法

其中rememberMeServices.loginSuccess的调用过程如下:
AbstractRememberMeServices#loginSuccess==>PersistentTokenBasedRememberMeServices#onLoginSuccess

AbstractRememberMeServices

public final void loginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		//这里就是上面说的检验request中是否含有remember-me参数,
		//如果有 才往下执行
		if (!rememberMeRequested(request, parameter)) {
			logger.debug("Remember-me login not requested.");
			return;
		}

		onLoginSuccess(request, response, successfulAuthentication);
	}

可以从上面看到 parameter的默认是指remember-me。如果没有选中‘记住我’复选框 则终止执行。

PersistentTokenBasedRememberMeServices

protected void onLoginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		String username = successfulAuthentication.getName();

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

		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
			tokenRepository.createNewToken(persistentToken);
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to save persistent token ", e);
		}
	}

这个方法的主要含义:
从登陆成功的信息中拿到用户名,随后生成一条插入语句 将token等信息插入到persistent_logins这个表中。并将token信息添加到cookie中。

persistent_logins这个是SpringSecurity默认提供出来针对“记住我”功能来记录token和cookie的数据表

tokenRepository这个在该类中的默认实现是new InMemoryTokenRepositoryImpl(),我们可以在Security配置中指定我们的tokenRepository。


PersistentTokenRepository有两种实现,分别是InMemoryTokenRepositoryImpl和JdbcTokenRepositoryImpl。两者的区别不言而喻,一个是将token信息放到内存中,一个是将其持久化到数据库



说了这么多,我们来配置下

protected void configure(HttpSecurity http) throws Exception {

//        ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter(failureHandler);

        //表单登录 任何请求均进行认证
       http.formLogin()
       //默认登录请求是/login,这里重置为/authentication/form 并不影响流程
                .loginProcessingUrl("/authentication/form")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .and()
                .rememberMe()
                .userDetailsService(userDetailsService)
                .tokenRepository(persistentTokenRepository()).
                and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .disable();
    }
		
@Autowired
    private DataSource dataSource;

    //这个Bean 就是上文我们说的替代默认tokenRepository的配置
	@Bean
    @ConditionalOnMissingBean(PersistentTokenRepository.class)
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
      //这句话 只有在第一次启动时 才放开注释 因为会创建表,再次运行时 注释,因为存在的表不能被再次创建
       // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

关于数据源的配置

spring.datasource.url=jdbc:mysql://localhost:3306/study
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

系统运行起来后:

在这里插入图片描述
点击登录,认证成功处理器关于自定义认证处理器,请参考这篇文章会将用户信息打印到页面
在这里插入图片描述

前面我们分析过,由于选中了"remember-me",用户名、密码登录成功后,会把用户的token存到表里 并且写入到cookie里一份。
我们去persistent_logins表中看下,发现token计入了表中
在这里插入图片描述
重启系统,发现不需要登录,直接可以请求接口,这说明了我们成功了。



源码分析
退出系统,重新再次访问系统资源时,会先进入到RememberMeAutnenticationFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
	
  //系统重新启动或用户退出过,所以SecurityContextHolder中没有用户的认证信息
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);

			if (rememberMeAuth != null) {
			
				try {
          //根据自动登录获取到的用户信息 进行认证 这里认证就很简单了 判断hashcode是否一致
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					
//将用户认证信息重新放到SecurityContextHolder中			
//SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
					onSuccessfulAuthentication(request, response, rememberMeAuth);
     ...

			chain.doFilter(request, response);
		}
	}

rememberServices#autoLogin方法如下

public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
  //从request中获取cookie
		String rememberMeCookie = extractRememberMeCookie(request);
 ...
			user = processAutoLoginCookie(cookieTokens, request, response);
...
  //封装用户认证信息 这里省略了
	}

processAutoLoginCookie方法
PersistentTokenBasedRememberMeServices

 UserDetails processAutoLoginCookie(String[] cookieTokens,
			HttpServletRequest request, HttpServletResponse response) {
	//存到数据库中的信息如下
//	前文中提到的`persistent_logins`表中series、token 分别代表下面的两个参数
		final String presentedSeries = cookieTokens[0];
		final String presentedToken = cookieTokens[1];
		
  //从表中取出token数据
		PersistentRememberMeToken token = tokenRepository
				.getTokenForSeries(presentedSeries);
...
		PersistentRememberMeToken newToken = new PersistentRememberMeToken(
				token.getUsername(), token.getSeries(), generateTokenData(), new Date());

		try {
      //更新表中的token
			tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
					newToken.getDate());
      //重新将新token 添加到cookie中
			addCookie(newToken, request, response);
		}
	...
    //获取user信息,从这里可以看出UserDetailsService或自定义UserDetailsService必须在RememberMe配置中配置
		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

RememberMeAuthenticationProvider的认证过程相较于其他provider,比较简单 这里一笔带过了


以上,是个人对”记住我“功能的个人实践与源码理解。如有问题,请指出,谢谢。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值