spring security中Remembers me 记住我基本原理

    实现思路:通过 Cookie 来记录当前用户身份。当用户登录成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头带回前端存储在cookie中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie中的信息进行校验分析,进而确定出用户的身份,Cookie中所保存的用户信息也是有时效的,例如三天、一周等

一、基本使用

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()// 表单登录
                .and()
                .rememberMe() // 记住我功能
                .and()
                .csrf().disable();
    }

1.1 开启后的效果

如果打开配置后,那么默认的登录页面就会出现记住我的checkbox选择框,我们在配置文件设置session 1分钟过期,先不勾选记住我

server:

  port: 8081

  servlet:

    session:

      timeout: 1m

1.2 未勾选记住我,我们查看cookie 里面的信息

这个时候cookie 里面只有一个jsessionId 

1.3 勾选记住我后,登录后我们等待一分钟

      我们等待一分钟后,重新登录,勾选记住我后,登录后查询cookie 里面是否有变化,里面多了一项remember-me,后续登录一分钟后,刷新接口 /hello 也没有掉线


二、实现原理

通过开启rememberme功能后,会往filter中注入RememberMeAuthenticationFilter 过滤器

1、请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用 autoLogin 方法进行自动登录。

2、当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用 authenticate 方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调井不包含 RememberMeServices 中的 loginSuccess 方法

3、如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调。onUnsuccessfulAuthentication 和onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是 RememberMeAuthenticalionFilter 过滤器所做的事情,成功将 RememberMeServices的服务集成进来。

2.1 RememberMeServices

这里一共定义了三个方法:

1. autoLogin 方法可以从请求中提取出需要的参数,完成自动登录功能

2  loginFail 方法是自动登录失败的回调。

3、lginSuccess 方法是自动登录成功的回调。

2.2  TokenBasedRememberMeServices

在开启记住我后如果没有加入额外配置默认实现就是由TokenBasedRememberMeServices进行的实现。查看这个类源码中 processAutoLoeinCookie 方法实现

总结:processAutoLoginCookie 方法主要用来验证 Cookie 中的令牌信息是否合法:
1. 首先判断 cookieTokens 长度是否为3,不为3说明格式不对,则直接抛出异常。
2. 从cookieTokens 数组中提取出第1个值,也就是过期时间,判断令牌是否过期,如果己经过期,则抛出异常。
3. 根据用户名cookieTokens 数组的第0项)查询出当前用户对象。
4. 调用makeTokenSignature 方法生成一个签名,签名的生成过程如下:首先将用户名、令牌过期时间、用户密码以及 key 组成一个宇符串,中间用“:”隔开,然后通过MD5消息摘要算法对该宇符串进行加密,并将加密结果转为一个字符串返回
5. 判断第4步生成的签名和通过 Cookie 传来的签名是否相等(即 cookieTokens 数组的第2项),如果相等,表示令牌合法,则直接返回用户对象,否则抛出异常

2.3 记住我原理图

登录:

 1org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)

 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication

org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices#loginSuccess

org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices#rememberMeRequested 是否开启了记住我功能

org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices#onLoginSuccess  默认实现

org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices#onLoginSuccess

org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices#setCookie 基于用户名密码生成cookie,默认两周

会话过期,如何自动生成一个cookie

org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter#doFilter(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)

org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices#processAutoLoginCookie  基于解析用户名后,重新从UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(cookieTokens[0]); 获取一次userDetail对象


三、内存令牌 PersistentTokenBasedRememberMeServices

1、不同于TokonBasedRemornberMeServices 中的 proce5sAutologinCookie 方法,这里1cookieTokens数组的长度为2,第一项是series,第二项是token。
2、从cookieTokens数组中分到提取出 series 和 token. 然后根据 series 去内存中查询出-个PersistentRememberMeToken对象。如果查询出来的对象为null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的 token 和从cookieTokens 中解析出来的token不相同,说明自动登录会牌已经泄澜 恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。
3、根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
4、生成一个新的 PersistentRememberMeToken 对象,用户名和series 不变,token 重新生成,date 也使用当前时间。newToken 生成后,根据 series 去修改内存中的 token和date(即每次自动登录后都会产生新的 token 和 date)
5、调用 addCookie 方法添加 Cookie, 在addCookie 方法中,会用到我们前面所说的setCookie 方法,但是要注意第一个数组参数中只有两项: series和 token (即返回到前端的令牌是通过对 series 和 token 进行 Base64 编码得到的)
最后将根据用户名查询用户对象井返回

3.1 内存中实现

@Bean
    public RememberMeServices rememberMeServices() {
        return new PersistentTokenBasedRememberMeServices("key"// 定义一个生成令牌的key
                ,userDetailsService()    //认证数据源
                ,new InMemoryTokenRepositoryImpl()  // 令牌存储方式
        );
}

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()//开启记住我的功能
                .rememberMeServices(rememberMeServices())
                .and()
                .csrf().disable();
    }

四、持久化令牌

   为什么会有这个呢,如果令牌在内存中,当系统重启之后,用户又需要重新登录,那么如果我将令牌信息保存到数据库中,下次直接从数据库获取,这样就可以实现系统重启后,不影响免登陆;

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
</parent>
<dependencies>
        <!-- web 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- SpringSecurity依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- thymeleaf 模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- thymeleaf 模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- 简化get set 方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

        <!-- 各种工具类包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>


        <!-- mybatis 支持 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

4.1 添加数据源

@Configuration
@Slf4j
@MapperScan(basePackages = MySqlDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "mysqlSqlSessionFactory" )
public class MySqlDataSourceConfig {


    static final String PACKAGE = "com.fashion.mapper.mysql";
    static final String MAPPER_LOCATION = "classpath:mybatis/mapper/mysql/*.xml";


    /**
     *  配置数据源
     * @return
     */
    @Primary
    @Bean
    public DataSource mysqlDataSource(){
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test_rbac");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("12345");
        return dataSource;
    }

    @Primary
    @Bean
    public SqlSessionFactory mysqlSqlSessionFactory(@Autowired DataSource mysqlDataSource){
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        try {
            sessionFactory.setDataSource(mysqlDataSource);
            //sessionFactory.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.xml"));
            sessionFactory.setMapperLocations(
                    new PathMatchingResourcePatternResolver().getResources(MySqlDataSourceConfig.MAPPER_LOCATION));
            return sessionFactory.getObject();
        } catch (Exception e) {
            log.error("mysql数据源初始化失败",e);
        }
        return null;
    }

4.2 设置PersistentTokenRepository存储方式

 repository.setCreateTableOnStartup(true); 为启动时候自动创建表结构,不需要手动创建表

@Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(mysqlDataSource);
        repository.setCreateTableOnStartup(true);//自动创建表结构
        return repository;
}


  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()//开启记住我的功能
                //.rememberMeServices(rememberMeServices())  // 指定rememberMeServices 实现
                .tokenRepository(persistentTokenRepository()) //持久化令牌
                .and()
                .csrf().disable();
    }

4.3  查看数据库表是否自动创建成功

4.4 登录后时候勾选记住我

test_remember  密码12345

4.5 一分钟后,刷新查看结果

    当设置session 过期时间为1分钟的时候,再刷新,看库里面的值是不是有变化,然后重启,再访问 /hello接口,看是否可以直接登录上

通过结果,我们看到token 已经变了,更新时间也变了

4.6  重启报错问题处理

   当重启时候报这个错误的时候,是因为我们设置了自动创建表结构,当我们在第一次创建成功后,就把该设置关闭 //repository.setCreateTableOnStartup(true);//自动创建表结构

过了一分钟后再看数据库和前端是否有更新

下一篇将介绍如何实战使用自定义页面记住我的功能,已经在前后端分离如何实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空寻流年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值