5-Remember Me

在开始之前我们补充一下四个知识:(1) Whitelist IP Range,这个很容易理解,添加IP地址限制。我们可以在WebSecurityConfig中的configure()中添加

(2)Message Localization,这个就是我们之前使用过的i18n,我们可以先看一下登录页面:

没有错,这也是登录页面,只不过是从我们登录页面点了“Spanish”

这一块在对我们国际化项目时比较有用。

(3)登出的处理,如果我们不处理,在登录后进去点击右上角的 Logout链接,会出现

我们需要新增一个logout.html,同时修改一下WMvcConfig配置,添加一条新纪录

然后我们就可以正常退出了。

(4)自定义的AuthenticationSuccessHandler,这个有点像我们之前用到过的CustomAuthenticationFailureHandler,我们可以在认证成功之后统一做一些处理,而不是简简单单的页面跳转——我们可以在session中存一些信息,比如下面username.这个可以用于页面显示用户名。

我们还可以更精确地控制不同用户跳转地页面:常见的就是“Admin”页面

我们后面还有对这个处理器进行修改,所有在后面的时候我们再来具体看这个类。

好了,现在回到,“记住我”这个主题上来。

如果是自己的个人电脑,每次让我输入用户名和密码的确安全一些,但是每次操作起来却很麻烦。我们现在使用的APP也都有类似记住我的功能,不用每次登录的确是件用户体验良好的事。那么我们先从代码改造上来看看我们需要实现记住我,需要做那些工作。

1.修改WebSecurityConfig配置:

我看看到上面有一个CustomRememberMeServices,可以看一下全代码:

我们再具体看一下CustomRememberMeServices类体系结构

我们可以看到最上面是RememberMeServices接口

我们先把代码改动讲完,最后我们再看看spring-security 内部怎么实现这个记住我的功能,不然在此处展开内部实现实现细节,会打乱“代码改造”节奏。

2.修改login.html页面,添加记住我——remember-me的checkbox

效果如下:

我们正常登录后可以看到控制台:

至此,我们的代码改造完成了,可以来验证一下。首先我们可以看到数据库中多了一张表(需要jdbcTokenRepository.setCreateTableOnStartup(true);这行代码执行后我删除了)

WebSecurityConfig.java

成功登录里面也是有数据的

然后我们重启服务,来模拟触发session失效,然后刷新浏览器,注意此时我们浏览器的地址是
http://localhost:18080/homepage.html?user=wys2317@hotmail.com

如果没有添加记住我,刷新页面肯定是跳转到登录页面,因为这个页面需要用户登录后才能访问,由于我们使用“持久化的记住我”功能,我们不会再跳转到登录页面了,服务器认为我们已经登录过了——因为我们的浏览器中有remember-me的cookie.

现在已经完成了 “持久化的记住我”功能 ,我们再来看看它的内部实现。

Spring Security 过滤器中 AbstractAuthenticationProcessingFilterRememberMeAuthenticationFilter 会调用这个接口中的实现类中的方法。我们关心的是RememberMeAuthenticationFilter,再一起来看看它的源码中核心代码

其中 rememberMeServices 会调用自动登录方法来返回一个Authentication
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);
我们再看看 autoLogin()的具体实现-->AbstractRememberMeServices

我们看到extractRememberMeCookie(request);它的具体实现

它就是从request中获取Cookie [],在前面我们看到浏览器控制台中cookie是一长串字符串
VjUlMkIwVFJnVlF6RnZ4SEN6JTJGaW92SEElM0QlM0Q6MFpWT1RyOFAzSFNaUGI5eiUyQjRlVU1BJTNEJTNE
然后我们数据库中保存是username,series,token,last_used。

显然返回给浏览器端的cookie是经过编码的,我们拿到浏览器端的cookie同样需要解码。String[] cookieTokens = decodeCookie(rememberMeCookie);

然后使用解码后的cookie完成自动登录
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
return createSuccessfulAuthentication(request, user);

最后我们可以小结一下“记住我”完成自动登录的内部细节:

首先是PersistentTokenBasedRememberMeServices在用户成功登录后,会生成一个PersistentRememberMeToken,然后保存在数据库中,同时把cookie回写到request中,这样浏览器端就有了cookie.

假设过了一段时间但是cookie仍然没有过期的情况下访问需要登录的资源,RememberMeAuthenticationFilter会调用RememberMeServices中的autoLogin(),尝试使用请求的cookie来完成用户自动登陆过程,再返回用户请求的资源。

在我们的代码项目中还是有可以改进的地方,在cookie有效的情况下,如果访问/login还是会跳转到登录页面,一个比较好的方式是直接跳转到成功登录后的页面。

小结:“ 记住我 ”是一个用户体验友好的功能,对于用户而言没有“持久化”,这是后端需要完成的工作,它大大减少了用户手动登录的次数(因为这项操作比较麻烦),尽管牺牲了一些安全性,返回给浏览器了编码后的用户信息——cookie,从后面的源码中我们可以看到解码后可以拿到用户名和过期时间。所以后面就有了我们需要防止用户暴力破解密码的功能。这个留到下一篇。“记住我”这个功能除了完成自动登录,其实它还在“自动地”更新cookie的有效期,默认的token有效期我们可以在源码TokenBasedRememberMeServices中看到是14天。

具体的代码可以参见github.

4-Password Encoding and Reset Password

讲到密码加密这一点,我们应该很容易接受,我们存在数据库中的密码不可能是明文方式,以前我们都是使用md5这样的算法来加密密码或者使用sha的方式来加密,现在它们都被新宠BCryptPasswordEncoder夺取了光彩。

我们在使用加密的密码时,其实使用方式很简单,配置也很简单。先看一下我们的使用方式

就是很简单地调用了encode(),由于我们没有之间写登录的实现,所以我们没有一个直接的密码比较的过程。相反我们只需要做好配置就行了。

配置就是注入一个Bean,同时我们看到了在authProvider()我们使用到了encoder(),有了它们spring-security就“自动”完成登录验证。

上面的配置和使用,都是比较简单的,然后让我们回到“重置密码”。

先梳理一下重置密码的流程:

  1. 登录页面,用户点击“重置密码”按钮 ;
  2. 跳转到重置密码页面 ;
  3. 用户输入email地址,再点击“重置”按钮 ;
  4. 后台发送邮件给用户,生成重置密码链接 ;
  5. 用户收到邮件后点击链接,跳转到重置密码,输入新密码,提交 ;
  6. 后台处理,完成密码重置的过程。

对于用户的交互的流程是1、3、5;其余是都是程序处理。对于整个流程而言是有点长,但还是可以接受。我们写代码的思路也就清醒起来。

首先是忘记密码页面forgetPassword.html,一个输入框和一个重置按钮,比较简单。

然后就是重置按钮提交给后台的处理:

我们仍然采用事件机制,发布一个重置密码事件。

重置密码事件的处理也很简单就是发送邮件:

然后用户邮箱应该受到这样一封邮件

当用户点击上面的链接应该跳转到修改密码页面

上面使用到了SecurityContextHolder这个工具类,设置了Authentication,这是因为后面处理保存密码的时候需要使用用户信息。

最后填写好新密码及确认密码后提交给后台处理

用户密码更新完成后再次跳转到登录页面,然后使用新密码就可以登录了。

至此,我们完成了重置密码的所有编码工作。最后让我看下一共修改了哪些文件。

①IUserService中共添加4个接口

②MvcConfig中添加了2行

③OldRegistrationController添加了3个接口

④WebSecurityConfig中添加了3个放行url

⑤java文件中新增了实体类PasswordResetToken和相应的Repo,还有OnResetPasswordEvent和ResetPasswordListener

html文件中新增了forgetPassword.html和updatePassword.html

小结:总体上,完成密码重置的编码工作不是很复杂,只要上面的流程清晰,加上用到了前面java mail,spring事件处理,还有spring security中的一个工具类SecurityContextHolder和UsernamePasswordAuthenticationToken,它们完成了请求上下文中认证信息的传递。

具体的代码可以参见github.

3-Resend Verification Email

上一篇文章中,我们讲到发送邮件激活账号,同时我们还引入了一个过期时间问题,那么如果我们的用户在很久之后才去点击激活链接,显然会受到一个token已经过期的提示,但是同时我们希望可以让用户重新发送一封激活邮件,这是可以让用户重新输入一次邮箱地址,但是更好的做法是只需要用户点一下“重新发送”按钮,然后受到一封有“新”的token的邮件即可。

此时用户看到的token也变化了,如果再用以前的token,就会是一个无效的token了,和原来token关联的那条记录token的有效期延长了。好了我们看看具体怎么做。

1.先看一下我们的一共修改的文件

在OldRegistrationController中我们添加下面的代码

同时我们重构了OnRegistrationCompleteEvent,添加了一个token和构造方法

同时我们还重构了RegistrationListener

在自定义异常处理中我们添加了MailAuthenticationException

最后我们的UserServiceImpl添加了两个实现

还有MvcConfig中添加了一行代码,用于MailError.html

至此我们的改动工作全部完成了。

我们点击上面的Resend按钮后,我们可以收到新的邮件。

在你点击上面的邮件链接后,你会看到

现在正准备登录,可能你忘记了上次输入的密码,那么这时候,我们就要写一个功能“忘记密码”。这个留到下一篇文章。

具体的代码可以参见github.