Spring Security(学习笔记) --RememberMe加密原理以及令牌丢失验证!

本文介绍了SpringSecurity中如何通过JdbcTokenRepository实现基于数据库的持久化令牌机制,以提高安全性。通过创建`persistent_logins`表并演示了如何配置和测试该功能,确保即使令牌被盗用也能及时响应并替换旧令牌。
摘要由CSDN通过智能技术生成

重点标识

上一篇文章我们了解到了,rememberMe 将令牌保存在cookie中,每次请求服务端都会去验证令牌的有效性以及合法性,但是,这样就会产生一个问题,如果浏览器中的令牌被人盗用了,那岂不是非常不安全。

关于这个问题,Security也给出了解决方案,那就是持久化令牌,一旦令牌被盗用,用户就可以及时感知,然后重新登陆,覆盖掉之前的令牌。

持久化令牌

在Spring Security中提供了一个持久化token的仓库接口,就是PersistentTokenRepository这个,他有两个实现类。
在这里插入图片描述
第一个,就是基于内存来弄,但是我们一般不这么搞,来看看,基于jdbc存入数据库持久化怎么弄。

进入到JdbcTokenRepository这个实现类中看看,注释我直接写到源码里面把,大家注意看。

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {

   //重点是这个,创建一个persistent_logins 表,用来记录token。里面有四个字段,分别解释一下
   //username  用户名
   //series 生成的字符串,用来找到token
   //token 具体的令牌
   //last_used  最后一次使用的时间
    public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)";
    public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
    public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
    public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
    private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";
    private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";
  ……
}

案例演示

然后,我们在数据库创建上面那个persistent_logins 表,然后创建一个工程,进行测试。

引入依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <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>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

然后简单配置一下,Security.config


@Configuration
public class SecurityConfig {

    @Autowired
    DataSource dataSource;

    //注入数据源
    @Bean
    JdbcTokenRepositoryImpl jdbcTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }


    /**
     * 这里用户简单点,我就写内存了
     * @return
     */
    @Bean
    UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").build());
        return inMemoryUserDetailsManager;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(a->a
                .requestMatchers("/auth/*").fullyAuthenticated()

                .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .rememberMe(r ->
                    r.
                            key("tongzhou")
                            //一直开启rememberMe记录,这样前端选不选都会记录
                            .alwaysRemember(true)
                            //设置token仓库,我们这里就是通过jdbc存入数据库中
                            .tokenRepository(jdbcTokenRepository())
                )
                .csrf(c->c.disable());
        return http.build();
    }
}

上面也写的比较清楚了,就不多说了,重点是看数据库,存在了一条记录。
在这里插入图片描述

然后关闭浏览器,去postman测一下,把remember-me复制过去

在这里插入图片描述
这样,也是可以的,但是我们关掉浏览器,然后重新打开,再去访问/hello这个接口,就会让你重新登陆了。
在这里插入图片描述可以看到,series值变了,token值也变了,这是因为我刚刚用浏览器过期的访问去请求他的地址,数据库直接把那条数据删掉了,后面分析源码,可以看一下。
在这里插入图片描述
ok了,这就没问题了,后台爆出被攻击信息。

Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.

注意

postman测的时候,要清掉JESSIONID,要清掉,采用rememberMe的认证信息后,他会把JSONID也认证了,所以有时候,我们重新登陆后,工具还能访问。清掉JSONID后,只用原本的RememberMe就登不上了。

源码解析

我们看一下,RememberMeServices ,这个服务接口提供了下面三个方法,
autoLogin 从请求中提取需要的参数
loginFail 自动登录失败后的回调
loginSuccess 自动登录成功后的回调。

public interface RememberMeServices {
    Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

    void loginFail(HttpServletRequest request, HttpServletResponse response);

    void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);
}

在这里插入图片描述

我们来下下它的实现类,主要看AbstractRememberMeServices,extractRememberMeCookie这个方法,获取到请求中的remember-me这个值,如果为null,直接返回。接着,看一下它的长度是不是0,如果是,则将前台的remember-me设置为null。

接下来,就是decodeCookie,对进来的rememberMeCookie进行解析,

 public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
        String rememberMeCookie = this.extractRememberMeCookie(request);
        if (rememberMeCookie == null) {
            return null;
        } else {
            this.logger.debug("Remember-me cookie detected");
            if (rememberMeCookie.length() == 0) {
                this.logger.debug("Cookie was empty");
                this.cancelCookie(request, response);
                return null;
            } else {
                try {
                    String[] cookieTokens = this.decodeCookie(rememberMeCookie);
                    UserDetails user = this.processAutoLoginCookie(cookieTokens, request, response);
                    this.userDetailsChecker.check(user);
                    this.logger.debug("Remember-me cookie accepted");
                    return this.createSuccessfulAuthentication(request, user);
                } catch (CookieTheftException var6) {
                    this.cancelCookie(request, response);
                    throw var6;
                } catch (UsernameNotFoundException var7) {
                    this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
                } catch (InvalidCookieException var8) {
                    this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
                } catch (AccountStatusException var9) {
                    this.logger.debug("Invalid UserDetails: " + var9.getMessage());
                } catch (RememberMeAuthenticationException var10) {
                    this.logger.debug(var10.getMessage());
                }

                this.cancelCookie(request, response);
                return null;
            }
        }
    }

附上解析代码,其实就是三部分,第一部分用户名,第二部分,时间戳,第三部分就是加密摘要。

 protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
        for(int j = 0; j < cookieValue.length() % 4; ++j) {
            cookieValue = cookieValue + "=";
        }

        String cookieAsPlainText;
        try {
            cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));
        } catch (IllegalArgumentException var7) {
            throw new InvalidCookieException("Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
        }

        String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":");

        for(int i = 0; i < tokens.length; ++i) {
            try {
                tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString());
            } catch (UnsupportedEncodingException var6) {
                this.logger.error(var6.getMessage(), var6);
            }
        }

        return tokens;
    }

这部分都拿到后,接下来就是对比了processAutoLoginCookie,这是一个抽象类,具体的实现,去它的子类看看。这里我们使用的数据库,所以看PersistentTokenBasedRememberMeServices

在这里插入图片描述
cookieTokens 参数不等于2就报错,这是因为里面有两个参数,series和token,要根据series去数据库里面查看token的值。如果查出来为null ,那就是没有,自动登陆失败。

如果查出来的值和现有的不一样,那就说明token已经泄露了,抛出一个错误,会在控制台打出来,可以去上面看看,是不是一样。

接着,再去看token的时间有没有过期,也没有过期的话,就会
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

用户名,series,不变,使用现在的时间,new Date,重新生成token。

this.tokenRepository.updateToken() 更新token,然后将新的token返回。

 if (token == null) {
                throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
            } else if (!presentedToken.equals(token.getTokenValue())) {
                this.tokenRepository.removeUserTokens(token.getUsername());
                throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
            } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            } else {
                this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));
                PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

                try {
                    this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                    this.addCookie(newToken, request, response);
                } catch (Exception var9) {
                    this.logger.error("Failed to update token: ", var9);
                    throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                }

                return this.getUserDetailsService().loadUserByUsername(token.getUsername());
            }

这就是他的认证了,接下来,我们看看它的成功后回调:

onLoginSuccess

登陆成功后创建一个PersistentRememberMeToken对象,调用addCookie将它存入数据库中。

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            this.tokenRepository.createNewToken(persistentToken);
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }
  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值