• Spring Security 中的 RememberMe 登录,so easy!


    1. RememberMe简介

    RememberMe 这个功能非常常见,图 6-1 所示就是 QQ 邮箱登录时的“记住我”选项。

    提到 RememberMe,一些初学者往往会有一些误解,认为 RememberMe 功能就是把用户名/密码用 Cookie 保存在浏览器中,下次登录时不用再次输入用户名/密码。这个理解显然是不对的。

    我们这里所说的 RememberMe 是一种服务器端的行为。传统的登录方式基于  Session 会话,一旦用户关闭浏览器重新打开,就要再次登录,这样太过于烦琐。如果能有一种机制,让用户关闭并重新打开浏览器之后,还能继续保持认证状态,就会方便很多,RememberMe 就是为了解决这一需求而生的。

    具体的实现思路就是通过 Cookie 来记录当前用户身份。当用户登录成功之后,会通过一定的算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头带回前端存储在 Cookie 中,当浏览器关闭之后重新打开,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie 中的信息进行校验分析,进而确定出用户的身份,Cookie 中所保存的用户信息也是有时效的,例如三天、一周等。敏锐的读者可能已经发现这种方式是存在安全隐患的。所谓鱼与熊掌不可兼得,要想使用便利,就要牺牲一定的安全性,不过在本章中,我们将会介绍通过持久化令牌以及二次校验来降低使用 RememberMe 所带来的安全风险。

    2. RememberMe基本用法

    我们先来看一种最简单的用法。

    首先创建一个 Spring Boot 工程,引入 spring-boot-starter-security 依赖。工程创建成功后,添加一个 HelloController 并创建一个测试接口,代码如下:

    1. @RestController
    2. public class HelloController {
    3.     @GetMapping("/hello")
    4.     public String hello() {
    5.         return "hello";
    6.     }
    7. }

    然后创建 SecurityConfig 配置文件:

    1. @Configuration
    2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    3.     @Bean
    4.     PasswordEncoder passwordEncoder() {
    5.         return NoOpPasswordEncoder.getInstance();
    6.     }
    7.     @Override
    8.     protected void configure(AuthenticationManagerBuilder auth) 
    9.                                                                  throws Exception {
    10.         auth.inMemoryAuthentication()
    11.                 .withUser("javaboy")
    12.                 .password("123")
    13.                 .roles("admin");
    14.     }
    15.     @Override
    16.     protected void configure(HttpSecurity http) throws Exception {
    17.         http.authorizeRequests()
    18.                 .anyRequest().authenticated()
    19.                 .and()
    20.                 .formLogin()
    21.                 .and()
    22.                 .rememberMe()
    23.                 .key("javaboy")
    24.                 .and()
    25.                 .csrf().disable();
    26.     }
    27. }

    这里我们主要是调用了 HttpSecurity 中的 rememberMe 方法并配置了一个 key,该方法最终会向过滤器链中添加 RememberMeAuthenticationFilter 过滤器。

    配置完成后,启动项目,当我们访问 /hello 接口时,会自动重定向到登录页面,如图 6-2 所示。

    可以看到,此时的默认登录页面多了一个 RememberMe 选项,勾选上 RememberMe,登录成功之后,我们就可以访问 /hello 接口了。访问完成后,关闭浏览器再重新打开,此时不需要登录就可以直接访问 /hello 接口;同时,如果关闭掉服务端重新打开,再去访问 /hello接口,发现此时也不需要登录了。

    那么这一切是怎么实现的呢?打开浏览器控制台,我们来分析整个登录过程。

    首先,当我们单击登录按钮时,多了一个请求参数 remember-me,如图6-3所示。

    很明显,remember-me 参数就是用来告诉服务端是否开启 RememberMe 功能,如果开发者自定义登录页面,那么默认情况下,是否开启 RememberMe 的参数就是 remember-me。

    当请求成功后,在响应头中多出了一个 Set-Cookie,如图 6-4 所示。

    在响应头中给出了一个 remember-me 字符串。以后所有请求的请求头 Cookie 字段,都会自动携带上这个令牌,服务端利用该令牌可以校验用户身份是否合法。

    大致的流程就是这样,但是大家发现这种方式安全隐患很大,一旦 remember-me 令牌泄漏,恶意用户就可以拿着这个令牌去随意访问系统资源。持久化令牌和二次校验可以在一定程度上降低该问题带来的风险。

    3. 持久化令牌

    使用持久化令牌实现 RememberMe 的体验和使用普通令牌的登录体验是一样的,不同的是服务端所做的事情变了。

    持久化令牌在普通令牌的基础上,新增了 series 和 token 两个校验参数,当使用用户名/密码的方式登录时,series 才会自动更新;而一旦有了新的会话,token 就会重新生成。所以,如果令牌被人盗用,一旦对方基于 RememberMe 登录成功后,就会生成新的 token,你自己的登录令牌就会失效,这样就能及时发现账户泄漏并作出处理,比如清除自动登录令牌、通知用户账户泄漏等。

    Spring Security中对于持久化令牌提供了两种实现:

    • JdbcTokenRepositoryImpl

    • InMemoryTokenRepositoryImpl

    前者是基于 JdbcTemplate 来操作数据库,后者则是操作存储在内存中的数据。由于 InMemoryTokenRepositoryImpl 的使用场景很少,因此这里主要介绍基于 JdbcTokenRepositoryImpl 的配置。

    首先创建一个 security06 数据库,然后我们需要一张表来记录令牌信息,创建表的 SQL 脚本在在 JdbcTokenRepositoryImpl 类中的 CREATE_TABLE_SQL 变量上已经定义好了,代码如下:

    1. public static final String CREATE_TABLE_SQL = "create table persistent_logins 
    2. (username varchar(64) not null, series varchar(64) primary key, " 
    3. + "token varchar(64) not null, last_used timestamp not null)";

    我们直接将变量中定义的 SQL 脚本拷贝出来到数据库中执行,生成一张 persistent_logins 表用来记录令牌信息。persistent_logins 表一共就四个字段:username 表示登录用户名、series 表示生成的 series 字符串、token 表示生成的 token 字符串、last_used 则表示上次使用时间。

    接下来,在项目中引入 JdbcTemplate 依赖和 MySQL 数据库驱动依赖:

    1. <dependency>
    2.     <groupId>org.springframework.boot</groupId>
    3.     <artifactId>spring-boot-starter-jdbc</artifactId>
    4. </dependency>
    5. <dependency>
    6.     <groupId>mysql</groupId>
    7.     <artifactId>mysql-connector-java</artifactId>
    8. </dependency>

    然后在 application.properties 中配置数据库连接信息:

    1. spring.datasource.url=jdbc:mysql:///security06?useUnicode=true&characterEncod
    2. ing=UTF-8&serverTimezone=Asia/Shanghai
    3. spring.datasource.username=root
    4. spring.datasource.password=123

    最后修改 SecurityConfig:

    1. @Configuration
    2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    3.     @Autowired
    4.     DataSource dataSource;
    5.     @Bean
    6.     JdbcTokenRepositoryImpl jdbcTokenRepository() {
    7.         JdbcTokenRepositoryImpl jdbcTokenRepository = 
    8.                                                    new JdbcTokenRepositoryImpl();
    9.         jdbcTokenRepository.setDataSource(dataSource);
    10.         return jdbcTokenRepository;
    11.     }
    12.     @Bean
    13.     PasswordEncoder passwordEncoder() {
    14.         return NoOpPasswordEncoder.getInstance();
    15.     }
    16.     @Override
    17.     protected void configure(AuthenticationManagerBuilder auth) 
    18.                                                                   throws Exception {
    19.         auth.inMemoryAuthentication()
    20.                 .withUser("javaboy")
    21.                 .password("123")
    22.                 .roles("admin");
    23.     }
    24.     @Override
    25.     protected void configure(HttpSecurity http) throws Exception {
    26.         http.authorizeRequests()
    27.                 .anyRequest().authenticated()
    28.                 .and()
    29.                 .formLogin()
    30.                 .and()
    31.                 .rememberMe()
    32.                 .tokenRepository(jdbcTokenRepository())
    33.                 .and()
    34.                 .csrf().disable();
    35.     }
    36. }

    在配置中我们提供了一个 JdbcTokenRepositoryImpl 实例,并为其配置了数据源,最后在配置 RememberMe 时通过 tokenRepository 方法指定 JdbcTokenRepositoryImpl 实例。

    配置完成后,启动项目并进行登录测试。登录成功后,我们发现数据库表中多了一条记录,如图6-5所示。

    此时如果关闭浏览器重新打开,再去访问 /hello 接口,访问时并不需要登录,但是访问成功之后,数据库中的 token 字段会发生变化。同时,如果服务端重启之后,浏览器再去访问 /hello 接口,依然不需要登录,但是 token 字段也会更新,因为这两种情况中都有新会话的建立,所以 token 会更新,而 series 则不会更新。当然,如果用户注销登录,则数据库中和该用户相关的登录记录会自动清除。

    可以看到,持久化令牌比前面的普通令牌安全系数提高了不少,但是依然存在风险。安全问题和用户的使用便捷性就像一个悖论,想要用户使用方便,不可避免地要牺牲一点安全性。对于开发者而言,要做的就是如何将系统存在的安全风险降到最低。

    那么怎么办呢?二次校验可以帮助我们进一步降低风险。。。

  • 相关阅读:
    专栏文章列表
    Devtron:很强大的 Kubernetes DevOps 平台
    java计算机毕业设计口腔医院网站源码+数据库+系统+lw文档+mybatis+运行部署
    java 枚举ENUM
    【CKA考试笔记】十九、master的负载均衡及高可用
    ACM第三周---周训---题目合集.
    高等数值计算方法学习笔记第4章【数值积分(数值微分)】
    传输层——再谈端口号
    AHU 算法分析 实验四 动态规划
    科沃斯、石头科技开打扫地机器人“价值战”
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/128048696