记住我功能
在实际开发中,有些项目为了用户登录方便还会提供记住我(Remember-Me)功能。如果用户登录时勾选了记住我选项,那么在一段有效时间内,会默认自动登录,并允许访问相关页面,这就免去了重复登录操作的麻烦。Spring Security提供了用户登录控制的同时,当然也提供了对应的记住我功能,前面介绍HttpSecurity类的主要方法rememberMe()就是Spring Security用来处理记住我功能的。下面,围绕rememberMe()这个方法来探索并讲解记住我功能的具体实现。
rememberMe()记住我功能相关涉及到记住我的主要方法及说明如表1所示。
表1 记住我相关的主要方法及说明
方法 | 描述 |
---|---|
rememberMeParameter(String rememberMeParameter) | 指示在登录时记住用户的HTTP参数 |
key(String key) | 记住我认证生成的Token令牌标识 |
tokenValiditySeconds(int tokenValiditySeconds) | 记住我Token令牌有效期,单位为s(秒) |
tokenRepository(PersistentTokenRepository tokenRepository) | 指定要使用的PersistentTokenRepository,用来配置持久化Token令牌 |
alwaysRemember(boolean alwaysRemember) | 是否应该始终创建记住我cookie,默认为false |
clearAuthentication(boolean clearAuthentication) | 是否设置cookie为安全的,如果设置为true,则必须通过HTTPS进行连接请求 |
需要说明的是,Spring Security 针对记住我(Remember-Me)功能提供了两种实现:一种是简单的使用加密来保证基于cookie中Token的安全;另一种是通过数据库或其它持久化机制来保存生成的Token。下面,分别对这两种记住我功能的实现进行讲解并演示说明。
1.基于简单加密Token的方式
基于简单加密Token的方式实现记住我功能非常简单,当用户选择记住我并成功登录后,Spring Security将会生成一个cookie并发送给客户端浏览器。其中,cookie值由下列方式组合加密而成:
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
上述cookie值的生成方式中,username代表登录的用户名;password代码登录用户密码;expirationTime表示记住我中Token的失效日期,以毫秒为单位;key表示防止修改Token的标识。
基于简单加密Token的方式中的Token在指定的时间内有效,且必须保证Token中所包含的 username、password和key没有被改变。需要注意的是,这种加密方式其实是存在安全隐患的,任何人获取到该记住我功能的Token后,都可以在该Token过期之前进行自动登录,只有当用户觉察到Token被盗用后,才会对自己的登录密码进行修改来立即使其原有的记住我Token失效。
下面,结合前面介绍的rememberMe()相关方法来实现这种简单的记住我功能。为了简化操作,在之前创建的项目用户登录页login.html中新增一个记住我功能勾选框,示例代码如下。
<form class="form-signin" th:action="@{/userLogin}" th:method="post" >
<img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
<h1 class="h3 mb-3 font-weight-normal">请登录</h1>
<!-- 用户登录错误信息提示框 -->
<div th:if="${param.error}"
style="color: red;height: 40px;text-align: left;font-size: 1.1em">
<img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
</div>
<input type="text" name="name" class="form-control"
placeholder="用户名" required="" autofocus="">
<input type="password" name="pwd" class="form-control"
placeholder="密码" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
<p class="mt-5 mb-3 text-muted">Copyright© 2019-2020</p>
</form>
上述修改的用户登录页login.html中,在用户登录的<form>表单中新增了一个checkbox多选框为用户提供勾选记住我选项。其中,记住我勾选框的name属性值设为了“rememberme”,而Security提供的记住我功能的name属性值默认为“remember-me”。
打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置,示例代码如下。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
// 需要对static文件夹下静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
// 自定义用户登录控制
http.formLogin()
.loginPage("/userLogin").permitAll()
.usernameParameter("name").passwordParameter("pwd")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
// 自定义用户退出控制
http.logout()
.logoutUrl("/mylogout")
.logoutSuccessUrl("/");
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200);
}
上述代码中,在之前实现的configure(HttpSecurity http)方法中使用rememberMe()及相关方法实现了记住我功能。其中,rememberMeParameter("rememberme")方法指定了记住我勾选框的name属性值,如果页面中使用了默认的“remember-me”,则该方法可以省略;tokenValiditySeconds(200)方法设置了记住我功能中Token的有效期为200s。
重启chapter07项目进行效果测试,项目启动成功后,通过浏览器访问“http://localhost:8080/userLogin
”进行登录,效果如图1所示。
图1 访问项目登录页效果
从图1可以看出,在项目登录页上已经出现了新添加的用于记住我功能的勾选框,直接在此登录界面输入正确的用户名和密码信息,同时勾选记住我功能,就会默认跳转到项目首页index.html。
为了演示记住我功能的实现效果,重新打开浏览器访问项目首页,直接查看影片详情(打开与之前登录用户对应权限的影片),效果如图2所示。
图2 访问影片详情效果
在初次登录时勾选了记住我功能后,在设置的Token有效期内再次进行访问不需要重新登录认证。如果在Token有效期过后再次访问项目时,会发现又需要重新进行登录认证。
2.基于持久化 Token的方式
持久化Token 的方式与简单加密Token的方式在实现Remember-Me功能上大体相同,都是在用户选择“记住我”功能并成功登录后,将生成的Token存入cookie中并发送到客户端浏览器,在下次用户通过同一客户端访问系统时,系统将直接从客户端cookie中读取Token进行认证。两者的主要区别在于:基于简单加密Token的方式,生成的Token将在客户端保存一段时间,如果用户不退出登录,或者不修改密码,那么在cookie失效之前,任何人都可以无限制地使用该Token进行自动登录;而基于持久化Token的方式采用如下实现逻辑:
(1)用户选择“记住我”成功登录后,Security会把username、随机产生的序列号、生成的Token进行持久化存储(例如一个数据表中),同时将它们的组合生成一个cookie发送给客户端浏览器。
(2)当用户再次访问系统时,首先检查客户端携带的cookie,如果对应cookie中包含的username、序列号和Token与数据库中保存的一致,则通过验证并自动登录,同时系统将重新生成一个新的Token替换数据库中旧的Token,并将新的cookie再次发送给客户端。
(3)如果cookie中的Token不匹配,则很有可能是用户的cookie 被盗用了。由于盗用者使用初次生成的Token进行登录时会生成一个新的Token,所以当用户在不知情时再次登录就会出现Token不匹配的情况,这时就需要重新登录,并生成新的Token和cookie。同时Spring Security 就可以发现cookie可能被盗用的情况,它将删除数据库中与当前用户相关的所有Token记录,这样盗用者使用原有的cookie将不能再次登录。
(4)如果用户访问系统时没有携带cookie,或者包含的username和序列号与数据库中保存的不一致,那么将会引导用户到登录页面。
从以上实现逻辑可以看出,持久化Token的方式比简单加密Token的方式相对更加安全。使用简单加密Token的方式,一旦用户的cookie被盗用,在Token有效期内,盗用者可以无限制地自动登录进行恶意操作,直到用户本人发现并修改密码才会避免这种问题;而使用持久化Token的方式相对安全,用户每登录一次都会生成新的Token和cookie,但也给盗用者留下了在用户进行第二次登录前进行恶意操作的机会,只有在用户进行第二次登录并更新Token和cookie时,才会避免这种问题。因此,总体来讲,对于安全性要求很高的应用,不推荐使用Remember-Me功能。
下面,结合前面介绍的rememberMe()相关方法来实现这种持久化Token方式的记住我功能。为了对持久化Token进行存储,需要在数据库中创建一个存储cookie信息的持续登录用户表persistent_logins(这里仍在之前创建的springbootdata数据库中创建该表),具体建表语句如下所示。
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null);
上述建表语句中创建了一个名为persistent_logins的数据表,其中username存储用户名,series存储随机生成的序列号,token存储每次访问更新的token,last_used表示最近登录日期。需要说明的是,在默认情况下基于持久化Token的方式会使用上述官方提供的用户表persistent_logins进行持久化Token的管理,读者不需要自定义存储cookie信息的用户表,如果有兴趣的读者可以自行查询相关方法。
在完成存储cookie信息的用户表创建以及页面记住我功能勾选框设置后,打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置,示例代码如下。
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义用户授权管理
http.authorizeRequests()
.antMatchers("/").permitAll()
// 需要对static文件夹下静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
// 自定义用户登录控制
http.formLogin()
.loginPage("/userLogin").permitAll()
.usernameParameter("name").passwordParameter("pwd")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
// 自定义用户退出控制
http.logout()
.logoutUrl("/mylogout")
.logoutSuccessUrl("/");
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200)
// 对cookie信息进行持久化管理
.tokenRepository(tokenRepository());
}
// 持久化Token存储
@Bean
public JdbcTokenRepositoryImpl tokenRepository(){
JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
jr.setDataSource(dataSource);
return jr;
}
上述代码中,与基于简单加密的Token方式相比,在持久化Token方式的rememberMe()示例中加入了tokenRepository(tokenRepository())方法对cookie信息进行持久化管理。其中的tokenRepository()参数会返回一个设置dataSource数据源的JdbcTokenRepositoryImpl实现类对象,在该类中就自定义了各种进行Token创建、更新、删除等方法。
重启chapter07项目进行效果测试,项目启动成功后,通过浏览器访问项目登录页,在登录界面输入正确的用户名和密码信息,同时勾选记住我功能后跳转到项目首页index.html。此时,查看数据库中persistent_logins表数据信息,效果如图3示。
图3 persistent_logins表数据
从图3以看出,项目启动后用户使用记住我功能登录时,会在持久化数据表persistent_logins中生成对应的用户信息,包括用户名、序列号、token和最近登录时间。
然后,重新打开刚才使用的浏览器,访问项目首页并直接查看影片详情(打开与之前登录用户对应权限的影片),会发现无需重新登录就可以直接访问。此时,再次查看数据库中persistent_logins表数据信息,效果如图4示。
图4 persistent_logins表数据
从图3和4对比可以看出,在Token有效期内再次自动登录时,数据库中的token会更新而其他数据不变,如果启用浏览器Debug模式还会发现,第二次登录返回的cookie值也会随之更新,这与之前分析的持久化的Token方法实现逻辑是一致的。
最后,返回到浏览器首页,单击首页上方的用户“注销”连接,在Token有效期内进行用户手动注销。此时,再次查看数据库中persistent_logins表数据信息,效果如图5示。
图5 persistent_logins表数据
从图5以看出,登录用户手动实现用户退出后,数据库中persistent_logins表的持久化用户信息也会随之删除。如果用户是在Token有效期后自动退出的,那么数据库中persistent_logins表的持久化用户信息不会随之删除,当用户再次进行访问登录时,而是在表中新增一条持久化用户信息。