当前位置: 首页 > 科技观察

SpringSecurity系列降低RememberMe的安全风险

时间:2023-03-16 22:16:19 科技观察

上一篇我们提到了SpringBoot自动登录的一些安全风险。在实际应用中,我们必须将这些安全风险降到最低。今天就来说说他们吧。让我们谈谈如何降低安全风险。为了降低安全风险,我主要从两个方面给大家介绍一下:持久化令牌方案的二次验证,当然还是老规矩了。在阅读本文之前,您必须阅读本系列的前几篇文章,这将有助于您更好地理解本文:好了,废话不多说,我们来看今天的文章。1.PersistenceToken1.1原理要了解persistencetoken,首先要了解自动登录的基本玩法,参考(SpringBoot+SpringSecurity实现自动登录功能)。持久令牌在基本的自动登录功能的基础上,增加了新的验证参数,提高了系统的安全性。这些都是开发者在后台完成的。对于用户来说,登录体验和普通的自动登录体验是一样的。在persistenttoken中,新增了两个通过MD5哈希函数计算得到的验证参数,一个是series,一个是token。其中,series只有在用户使用用户名/密码登录时才会生成或更新,只要有新的session就会重新生成token,防止一个用户同时登录多个终端同时,就像手机QQ一样,如果你在一个手机上登录,另一个手机上的登录会被踢出,这样用户就可以很容易地发现账号是否被泄露了(我在松哥看到有朋友交流群讨论如何禁止多终端登录,其实可以借鉴这里的思路)。持久化令牌的具体处理类在PersistentTokenBasedRememberMeServices中。上一篇我们提到的自动登录的具体处理类在TokenBasedRememberMeServices中。它们有一个共同的父类:用来保存token的处理类是PersistentRememberMeToken,这个类的定义也很简洁命令:publicclassPersistentRememberMeToken{privatefinalStringusername;privatefinalStringseries;privatefinalStringtokenValue;privatefinalDatedate;//省略getter}其中Date表示最后一次使用了自动登录。1.2代码演示下面我将通过代码向大家展示持久化令牌的具体用法。首先,我们需要一个表来记录token信息。我们可以完全自定义这个表,也可以使用系统提供的JDBC来操作。如果我们使用默认的JDBC,即JdbcTokenRepositoryImpl,我们可以分析这个类的定义:publicclassJdbcTokenRepositoryImplextendsJdbcDaoSupportimplementsPersistentTokenRepository{publicstaticfinalStringCREATE_TABLE_SQL="createtablepersistent_logins(usernamevarchar(64)notnull,seriesvarchar(64)primarykey,"+"tokennullstampusnotedtime(64))";publicstaticfinalStringDEF_TOKEN_BY_SERIES_SQL="selectusername,series,token,last_usedfrompersistent_loginwhereseries=?";publicstaticfinalStringDEF_INSERT_TOKEN_SQL="insertintopersistent_logins(username,series,token,last_used)values(?,?,?,?)";publicstaticfinalStringDEF_UPDATE_TOKEN_SQL="updatepersistent_loginssettoken=?,last_used=?whereseries=?";publicstaticfinalStringDEF_REMOVE_USER_TOKENS_SQL="deletefrompersistent"根据这段定义,我们可以分析表的结构,松哥这里给出一个SQL脚本:CREATETABLE`persistent_logins`(`username`varchar(64)COLLATEutf8mb4_unicode_ciNOTNULL,`series`varchar(64)COLLATEutf8mb4_unicode_ciNOTNULL,`token`varchar(64)COLLATEutf8mb4_unicode_ciNOTNULL,`last_used`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(`series`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;首先我们在数据库准备这张表由于我们要连接数据库,所以还需要准备jdbc和mysql的依赖,如下:>mysqlmysql-connector-java然后修改application.properties配置数据库连接信息:spring.datasource.url=jdbc:mysql:///oauth2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaispring.datasource.username=rootspring.datasource.password=123接下面来,我们修改SecurityConfig,如下:@AutowiredDataSourcedataSource;@BeanJdbcTokenRepositoryImpljdbcTokenRepository(){JdbcToken=copentoryRempljjdbnewJdbcTokenRepositoryImpl();jdbcTokenRepository。setDataSource(dataSource);returnjdbcTokenRepository;}@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().key("javaboy").tokenRepository(jdbcTokenRepository()).and().csrf().disable();}提供一个JdbcTokenRepositoryImpl实例,并为其配置DataSource数据源,最后通过tokenRepository将JdbcTokenRepositoryImpl实例包含到配置中即可,done我们就可以测试这一切了1.3测试我们还是先进入/hello界面,然后会自动跳转到登录页面,然后我们进行登录操作,记得勾选“记住我”选项,并且登录成功最后我们可以重启服务器,然后关闭浏览器再打开,然后访问/hello界面,发现还是可以访问的,说明我们的持久化令牌配置已经生效了。检查remember-me的token,如下:这条命令解析完卡片后,格式如下:emhqATk3ZDBdR8862WP4Ig%3D%3D:ZAEv6EIWqA7CkGbYewCh8g%3D%3D其中%3D表示=,所以上面的字符其实可以是翻译成以下wing:emhqATk3ZDBdR8862WP4Ig==:ZAEv6EIWqA7CkGbYewCh8g==现在查看数据库,我们发现在之前的表中生成了一条记录:数据库中的记录与我们解析后看到的remember-metoken一致。1.4源码分析这里的源码分析和上一篇的流程基本一样,只是实现类变了,即生成token/解析token的实现变了,所以这里主要给大家看下区别,流程问题,可以参考之前的文章。这次的实现类主要是:PersistentTokenBasedRememberMeServices,我们先来看里边几个和令牌生成相关的方法:protectedvoidonLoginSuccess(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationsuccessfulAuthentication){Stringusername=successfulAuthentication.getName();PersistentRememberMeTokenpersistentToken=newPersistentRememberMeToken(username,generateSeriesData(),generateTokenData(),newDate());tokenRepository.createNewToken(persistentToken);addCookie(persistentToken,request,response);}protectedStringgenerateSeriesData(){byte[]newSeries=newbyte[seriesLength];random.nextBytes(newSeries);returnnewString(Base64.getEncoder().encode(newSeries));}protectedStringgenerateTokenData(){byte[]newToken=newbyte[tokenLength];random.nextBytes(newToken);returnnewString(Base64.getEncoder().encode(newToken));}privatevoidaddCookie(PersistentRememberMeTokentoken,HttpServletRequestrequest,HttpServletResponseresponse){setCookie(newString[]{token.getSeries(),token.getTokenValue()},getTokenValiditySeconds(),request,response);}可以看到:登录成功后,首先获取的是用户名,即username。接下来,构造一个PersistentRememberMeToken实例。其中generateSeriesData和generateTokenData方法分别用于获取series和token,具体的生成过程其实就是调用SecureRandom生成随机数,然后进行Base64编码。与我们之前使用的伪随机数不同,例如Math.random或java.util.Random,SecureRandom使用类似于密码学的随机数生成规则。其输出结果难以预测,适用于登录等场景。在tokenRepository实例中调用createNewToken方法。tokenRepository其实就是我们一开始配置的JdbcTokenRepositoryImpl,所以这行代码其实就是在数据库中存储PersistentRememberMeToken。最后,addCookie,如您所见,添加系列和标记。这是token生成和token验证的过程,也是在这个类中,方法是:processAutoLoginCookie:protectedUserDetailsprocessAutoLoginCookie(String[]cookieTokens,HttpServletRequestrequest,HttpServletResponseresponse){finalStringpresentedSeries=cookieTokens[0];finalStringpresentedToken=cookieTokens[1];PersistentRememberMeTokentoken=tokenRepository.getTokenForSeries(presentedSeries);if(!presentedToken.equals(token.getTokenValue())){tokenRepository.removeUserTokens(Meken.getUsername());thrownewCookieTheftServicecookie,"Invalidremember-metoken(Series/token)mismatch.Impliespreviouscookietheftattack."));}if(token.getDate().getTime()+getTokenValiditySeconds()*1000L