Spring Security实现RememberMe功能以及原理探究

在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,下面,主要讲解如何使用Spring Security实现记住我这个功能以及深入源码探究它的原理。

首先,如下图所示为Spring Security实现RememberMe功能的原理图:
这里写图片描述
这里写图片描述

首先你进入登录页面,输入用户名以及密码进行登录,通过前几篇文章我们知道了Spring Security首先会进入UsernamePasswordAuthenticationFilter进行验证,如果认证成功它会调用一个叫RememberMeService的类,这个类会调用TokenRepository这个类,而这个类会将一个生成的Token写入数据库中。当你下次再进入该网站的时候,它会直接进入RemeberMeAuthticationFilter这个过滤器中,通过Cookie读取之前的Token,然后拿着这个Token通过RememberService到数据库中查找用户信息,如果存在该Token,则取出该Token对应的用户名以及其他信息放入UserDetailService,然后进入调用的URL。

下面,先来看看如何实现这一功能,最后会通过源码来查看它的实现原理。
Spring Security实现RememberMe功能比较简单。首先定义在配置类中定义一个Repository类进行数据库的读写。实现一个UserServiceDetail类,然后进行相关配置即可。

package cn.shinelon.security.browser;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import cn.shinelon.security.authenticate.ImoocAuthenctiationFailureHandler;
import cn.shinelon.security.authenticate.ImoocAuthenticationSuccessHandler;
import cn.shinelon.security.core.SecurityProperties;
import cn.shinelon.security.core.validate.code.ValidateCodeFilter;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SCryptPasswordEncoder();
    }
    @Autowired
    private DataSource dataSource;

    //记住我后的登录页面
    @Autowired
    private UserDetailsService userDetailsService;
    //记住我的功能
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
        jdbcTokenRepositoryImpl.setDataSource(dataSource);
        //启动时创建一张表,这个参数到第二次启动时必须注释掉,因为已经创建了一张表
//      jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
        return jdbcTokenRepositoryImpl;
    }   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/autentication/forms")
            .and()
        .rememberMe() 
            .tokenRepository(getPersistentTokenRepository())
            .tokenValiditySeconds(3600)   //Token过期时间为一个小时
            .userDetailsService(userDetailsService)
            .and()
            .authorizeRequests()
            .antMatchers(
                    "login.html"
                    ).permitAll()       //因为所有的请求都会需要认证处理,所以允许访问这些页面
            .antMatchers("/login.html").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();      //取消放置csrf攻击的防护
    }

}

然后就是UserDetailService的实现类:

package cn.shinelon.security.browser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class MyUserDetailServices implements UserDetailsService{
    @Autowired
    private PasswordEncoder passwordEncoder;

    private Logger log=LoggerFactory.getLogger(getClass());
    /**
     * 从数据库中获取密码与前端提交的密码进行对比
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("用户名:"+username);
        //指定用户授权登录信息,这里密码指定了,实际中需要到数据库中验证密码
        String password=passwordEncoder.encode("123456");
        log.info("数据库中密码是:"+password);
        return new User(username, password,
                true,true,true,true,        //用户被锁定     
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

资源文件中连接数据库的信息:
application.properties:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=yes&characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=123456

登录页面:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <form action="/autentication/forms" method="POST">
    用户名:<input type="text" name="username"/></br>
    密码 :<input type="password" name="password"/></br>
    图形验证:<input type="text" name="imageCode" /><img src="/code/image"/></br>
    <input type="submit" value="登录"/>
    <!-- name必须是remeber-me -->
    <input type="checkbox" name="remember-me" value="true"/>记住我
    </form>
</body>
</html>

至此,就已经配置好RememberMe这个功能,启动项目进入登录页面点击记住我这个选项就会发现数据库中Spring Security自动为创建了一张表并且记录了Token。
这里写图片描述
这里写图片描述
然后你可以重新启动项目,然后重新访问你需要的资源,就不会被拦截到登录页面可以直接访问,因为它已经在数据库中存入了Token。

下面,我们来深入源码看看Spring Security到底是怎么实现这一功能的。

关于Spring Security的认证流程不了解的读者可以查看前面的文章,当进行登录的时候,它会进入UsernamePasswordAuthticationFilter进行用户信息验证,验证成功之后它会调用一个AbstractAuthenticationProcessingFilter的过滤器进入一个successfulAuthentication方法中将用户的信息存入session中,然后调用RememberService调用loginSuccess方法,在这个方法中它又会调用rememberMeRequested方法,这个方法返回一个Boolean值,判断是否设置好RemeberMe这个功能。在rememberMeRequested这个方法中它会获取登录页面传送过来的remember-me这个参数,因为我们设置了value=”true”,因此它不为空并且该方法返回true。

这里写图片描述
这里写图片描述
这里写图片描述

扫描二维码关注公众号,回复: 129541 查看本文章

上面的方法一路返回到进入一个PersistentTokenBasedRememberMeServices的类中调用TokenRepository将Token存入数据库中,并且将Token存入cookie中。在TokenRepository中调用我们上面实现的JdbcTokenRepositoryImpl bean的createNewToken方法内部实际上使用JdbcTemplate在更新数据库插入了当前Token。

这里写图片描述
这里写图片描述
在存入cookie是读取设置的过期参数,然后创建一个cookie通过response响应到浏览器存入cookie中。
这里写图片描述

当我们重新启动项目访问页面时,由于session被销毁,但是cookie中存在Token,它会到RememberMeAuthenticationFilter中读取session中的值,由于是重启项目,session被销毁,因此session为空,它会去到cookie中取是否存在Token,遍历cookie获取到RememberMe的Token,然后根据Token获取用户信息返回调用的URL信息。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

上面就是Spring Security实现RemeberMe功能的原理,关键是掌握功能的原理图,然后查看源码即可清楚的理解它的流程。


猜你喜欢

转载自blog.csdn.net/qq_37142346/article/details/80114609
今日推荐