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);//自动创建表结构

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

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

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1444. Elephpotamus Time limit: 0.5 second Memory limit: 64 MB Harry Potter is taking an examination in Care for Magical Creatures. His task is to feed a dwarf elephpotamus. Harry remembers that elephpotamuses are very straightforward and imperturbable. In fact, they are so straightforward that always move along a straight line and they are so imperturbable that only move when attracted by something really tasty. In addition, if an elephpotamus stumbles into a chain of its own footprints, it falls into a stupor and refuses to go anywhere. According to Hagrid, elephpotamuses usually get back home moving along their footprints. This is why they never cross them, otherwise they may get lost. When an elephpotamus sees its footprints, it tries to remember in detail all its movements since leaving home (this is also the reason why they move along straight lines only, this way it is easier to memorize). Basing on this information, the animal calculates in which direction its burrow is situated, then turns and goes straight to it. It takes some (rather large) time for an elephpotamus to perform these calculations. And what some ignoramuses recognize as a stupor is in fact a demonstration of outstanding calculating abilities of this wonderful, though a bit slow-witted creature. Elephpotamuses' favorite dainty is elephant pumpkins, and some of such pumpkins grow on the lawn where Harry is to take his exam. At the start of the exam, Hagrid will drag the elephpotamus to one of the pumpkins. Having fed the animal with a pumpkin, Harry can direct it to any of the remaining pumpkins. In order to pass the exam, Harry must lead the elephpotamus so that it eats as many pumpkins as possible before it comes across its footprints. Input The first input line contains the number of pumpkins on the lawn N (3 ≤ N ≤ 30000). The pumpkins are numbered from 1 to N, the number one being assigned to the pumpkin to which the animal is brought at the start of the trial. In the next N lines, the coordinates of the pumpkins are given in the order corresponding to their numbers. All the coordinates are integers in the range from −1000 to 1000. It is guaranteed that there are no two pumpkins at the same location and there is no straight line passing through all the pumpkins. Output In the first line write the maximal number K of pumpkins that can be fed to the elephpotamus. In the next K lines, output the order in which the animal will eat them, giving one number in a line. The first number in this sequence must always be 1.写一段Java完成此目的
06-03
题目翻译:哈利波特正在参加魔法生物关怀考试,他的任务是喂养一只小矮象河马。小矮象河马非常直接和冷静,只有在被真正美味的东西吸引时才会移动。此外,如果小矮象河马跌入自己的脚印链,它会陷入昏迷并拒绝前进。根据海格的说法,小矮象河马通常沿着自己的脚印回家。这就是为什么它们从不越过脚印,否则它们可能会迷路。当小矮象河马看到自己的脚印时,它会尽力记住自离家出发以来的所有行动,然后计算出其巢穴的方向,然后转身朝着巢穴直走。一个小矮象河马执行这些计算需要一些时间。而一些无知的人认为这是一种昏迷,实际上这是这种美妙的,虽然有点迟钝的生物出色的计算能力的展示。小矮象河马最喜欢的美食是大象南瓜,而哈利所在的草坪上就有一些这样的南瓜。在考试开始时,海格将小矮象河马拖到其一只南瓜旁边。喂了小矮象河马一只南瓜后,哈利可以把它引导到剩下的任意一只南瓜。为了通过考试,哈利必须引导小矮象河马尽可能多地吃南瓜,直到它遇到自己的脚印。 输入:第一行包含草坪上南瓜的数量N(3≤N≤30000)。南瓜从1到N编号,数字1被分配给小矮象河马开始时所在的南瓜。在接下来的N行,按其编号对应的顺序给出南瓜的坐标。所有坐标都是范围从-1000到1000的整数。保证没有两个南瓜在相同的位置,并且没有一条直线穿过所有南瓜。 输出:第一行写出小矮象河马最多可以吃到的南瓜数K。在接下来的K行,输出小矮象河马将要吃的南瓜的顺序,每行输出一个数字。这个序列的第一个数字必须始终为1。 解题思路:这道题是一道比较难的贪心算法,需要仔细思考。我们可以先把所有的南瓜按照离开始点的距离排序,然后从第二个南瓜开始遍历,每次找到一个能够吃到的南瓜就将其加入结果,并更新当前位置和能够到达的最远位置。如果当前位置已经在之前的路径出现过,那么就不能再继续走了,因为这样会导致小矮象河马迷路。最后输出结果即可。 代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

星空寻流年

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

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

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

打赏作者

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

抵扣说明:

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

余额充值