防止暴力破解,这个话题在上一篇末尾说到,当用户有了你的用户名,如果一直尝试用密码去登录,如果你的密码强度比较高的情况下,“破解”这种难度会比较高,密码强度很大程度上是用户为了安全做出的努力,那我们开发者有没有一个可以贡献的地方呢?
显然是有的,我们的常用到的手机,有手势密码,有数字密码,有FaceID,如果我们尝试了很多次都一直错误,可能账号会被锁定。锁定就是暂时不让用户“尝试破解”密码,这时锁定就是不管用户输入什么密码,我们直接拒绝认证请求(登录操作),不进行后面的密码验证、用户鉴权等等操作。甚至我们在苹果手机上还会看到,如果用户一直尝试解锁失败,锁定的时间也会越来越长,这个方案也会带来新的挑战,如果“坏人”就是要锁定你的账号,不让你登录——很长时间的锁定,这时候我们可能会有新的一些安全措施,比如常用的登录设置,常用的登录IP地址(账号使用范围,比如安徽省,而不是美国加州),甚至有些时候我们可以人工客服解锁账号。这一些都是常用的防止暴力破解的安全方案及后备方案,我们今天探讨的是在Spring Security中怎么实现防止用户暴力破解。下面我们看看具体的操作。
1.添加一个认证失败事件监听器AuthenticationFailureEventListener
2.添加一个认证成功事件监听器AuthenticationSuccessEventListener
3.上面两个事件监听器都用到了LoginAttemptService
再看看它的具体实现类LoginAttemptServiceImpl
上面用到了Google guava框架中的LoadingCache,CacheBuilder,CacheLoader.其实具体的实现不是很困难,上面的方便之处就是可以在写入一天后过期,这一点在数据维护方面,投入的成本就比较低,比如前面我们用到的用户激活token,忘记密码token,这些数据中都有过期信息,那些过期数据其实没有太多数据价值的话,为了节约数据库成本是可以删除掉的,当然可以交给dba去做,还有就是可以使用java中的定时任务完成过期数据的删除,这个在后面的文章中我们会使用到。
上面使用的是内存保存数据,在服务重启的情况下,可能不是最好的选择,我们就自然想到Redis,数据既在内存中,同时还可以持久化数据到硬盘,并不会丢失。所以在实践中可以使用Redis来替代上面的 LoadingCache 。
还有一小点,MAX_ATTEMPT = 10,这个如果经常变更可以写成可配置的。
4.修改一下MyUserDetailsServiceImpl,优先检查用户有没有被阻塞。
5.CustomAuthenticationFailureHandler添加相应的错误信息(这个之前已经写好了)
UNUSUAL_LOCATION 这个我们后面也会使用到,先可以不用关心。
最后我们一起看下所有新增及改动的代码
我们启动项目验证一下,我故意把wys2317@hotmail.com的密码输错3次,我修改了MAX_ATTEMPT=3. 第四次再输入错误时就会出现下面的信息,然后你可以再尝试都会出现下面的信息。
最后我们补充两点:(1)上面的错误信息是在messages.properties中定义的。auth.message.blocked=This ip is blocked for 24 hours
说到这个i18n,我们如果想让错误信息更“动态”,其实 messages.properties 中可以使用占位符的,这样如果我们构建的“锁定”时间是一次比一次长的情况下,我们就可以给出具体的锁定时间,而不是每次都是24 hours。这个感兴趣地可以自己搜索有关资料。(2)看一下这个获取客户端ip的方法
在获取ip的时候,可能你会使用工具类,因为有时候需要一些封装,上面的封装可能还不够, 想获取真实的用户ip有时候的确需要付出一些额外的努力,代码封装上面的,甚至如果你的nginx也需要做一些配置。这个也是需要我们根据真实情况做一些调整。
具体的代码可以参见github.