是什么促成了你成为一名“厉害的”开发者?

当看到“厉害的”这个词的时候,我希望你知道我指的是“令人倾佩”,“令人欣赏”的意思。同时我也觉得你一定是见过这样的人。

那么我们到底欣赏他(她)的是什么呢?

肯定是他(她)一定是解决问题能力很强的人。在实际编程问题中,我们都是在解决问题,我曾向我母亲解释我的职业,或者说我的工作——有点像小学的时候解决数学应用题。编程很多的时候都是在解决问题。只不过我们不是用数学的语言,而是用编程语言。

解决一个问题,如果你知道的知识比较多,比如你现在已经是中学毕业了,假设你学业成绩还不错,你再去看小学的数学应用题,我想应该不是很难,你可以比较轻松地解题。高出那个级别,你就很容易,因为你掌握足够的知识。所以不断让自己级别提高就是掌握更多的知识。

解决问题,尤其是相应的知识掌握的比较多,你解决问题的能力自然不会很低。知识一定只是解决问题的一个方面,有时候尽管你掌握了很多的知识,包括解决相应问题方面的,你一定会那是那个令人倾佩,令人欣赏的开发者吗?

我看不一定是。知识只是一个方面。解决问题,除了拥有知识,还需要拥有解决问题的动力。你知道该怎么做,但是不一定说你一定会去及时做。你没有及时做的原因,可能是你懒了,也可能是你没有排好事情的优先级,你可能觉得自己不懒,现在你可能觉得我说“懒”是一个贬义词,其实懒是让你蜕变的一个内在因素,相反如果你一直那么忙碌,一直那么勤劳,有时并不是表面上看到的。懒一定程度上触发了思考,触发了创新。懒是惰性导致的,你有时都没有感觉到自己懒。但不管怎样,我说这么多,就是让你认识到,我说懒,并不是说你怎么不好,请一定要理解。

我们经常没有意识到一个问题,那就是客观的时间和自己的精力都是有限的,至少是今天有限。有时候我们在解决一个问题,在思考一个bug怎么解决,我们就会投入进入,然后时间就在默默流逝,当然你的精力也是在以不易察觉的形式流逝。有点像手机里的电池一样,你每天早上醒来,经过一夜的充电,活力满满,元气十足,那是你休息了一晚,睡了个好觉。然后经过一天的工作,思考一些问题,写一些代码,你的电池就会慢慢放电,直到电量90%,80%,70%,下午的时候甚至就只有40%,接近下班的时候就或许只剩下15%。关于时间和精力的问题,我以前只认为是程序员乐观导致错误地估计,后来有一天我突然认识到,乐观不仅仅程序员的“特权”,很多人都拥有着,并使用着。

我上面提到有时候没有解决问题,可能是你动力不足,也可能是你安排不当。如果我们深刻地意识到时间和精力是有限的,我们一定要懂得合理安排事情,解决工作的问题也是一样,这里可能就是指编程中问题,你一定要认识到那些事情是你可以及时解决的,那些问题需要你及时解决的,不是别人怀疑你的能力,而是工作的方式,需要为自己的工作事项,合理安排时间和顺序,这样你才能提高解决问题的能力,这种能力和知识无关,可以叫做工作方法。如果工作方法也叫知识的话,是的。它也是知识,你现在掌握了,以后应用的时候,你如果没有及时解决问题,不要怪罪于不知道怎么做的知识层面,而应该更多的时候,想想你工作方法有问题。

我们再深入一点,我上面“普及”的知识是你第一次听到吗?如果真的是,你真应该好好庆幸,你get到了这块知识。我们在上学的时候,可能就听说过,“事情要分清楚轻重缓急”。我想上面的工作方法也是由它进化而来的。工作中给各种事情排个号,在什么时间做什么事,重要的事情,需要优先完成。所以说这块知识,你可能已经掌握了。那你为什么没有做呢?因为做这项事情是需要思考的,思考是件需要克服惯性的,如果你的惯性就是不给事情排优先级,按照事情进行的顺序,或者说事情来到的顺序来做,那么你很有可能是没有很好地提高解决问题地能力。

我大学的时候曾经一度认为,我是一个分不清事情轻重缓急的人,我那个时候经常会遇到很多的事情并发地发生,需要我去处理。然后有时候,我没有做好一些事情,比如我当时在学生会中的一些工作,在比如学习上的一些事情。

我今年看了一些书籍,那些书籍竟然是理财方面的,我认识到了,在行动的内部,是由思想控制的,但很多时候行为却是由惯性控制的,它有点跳过思想,它的喜好更多的时候就是不喜欢思考。如果你养成了这个习惯,可能还自己都没有认识到,它伪装的如此完美,就像隐身术一样,你看不到它,但是它却在暗中“左右”着你的行为。

如果你养成了把事情排个优先级的习惯,解决工作中的问题的时候也是如此,那么你解决问题的能力会有较大的提升。这就是习惯的力量。而且你发现这将是一个好的习惯,它会你把有限的时间和精力发挥地淋漓尽致。

习惯的养成,需要我们付出努力,因为你可能经常放弃,或者产生放弃的想法,但是你要知道,你要想提高解决问题的能力,在知识方面你也在积累,但是更多的时候,好的工作方法——这是习惯提供给你的,会让你事半功倍。

我现在分享到的“习惯”还只是知识层面,应该它只有落实到行动上,才会是属于自己的习惯。这个习惯需要时间的洗礼。

我还想起了一段话,这段话是——编程中,不要相信你的眼睛,而要相信你的Ctrl+C和Ctrl+V。这是我们写代码中经常出现的,有时候,我们经常去敲一些单词,可能是变量,但是最后程序的运行结果却不是我们期待的。

这表面上也是编程中的方法问题,其实也是习惯问题。当你养成了相信计算机而不是自己的时候,你犯错的概率也会大大下降,当然现在的IDE很多的时候都在帮你少犯一些“低级”错误。

回到主题上来——是什么促成了你成为一名“厉害的”开发者?

上面主要提到的是知识之外的——解决问题的能力养成,在有限的时间和精力中把事情习惯地排上优先级,然后再一一完成事情。

解决问题,还需要知识。我记得我在《HOW TO DEVELOP QUALITY SOFTWARE WITH A GOOD VELOCITY》中提到过知识是需要积累的,但你可以掌握快速地解决问题的最少知识,快速地获取知识也绝对是一门可以练习的技能。这个话题我想此时不在这篇文章中展开。

虽然文章大篇幅地在讲知识之外的能力——养成好的工作方法,形成习惯。但是我们在用好的工作方式的同时,我们一定也不会忘记知识的力量。

6-Prevent Brute Force Authentication Attempts

防止暴力破解,这个话题在上一篇末尾说到,当用户有了你的用户名,如果一直尝试用密码去登录,如果你的密码强度比较高的情况下,“破解”这种难度会比较高,密码强度很大程度上是用户为了安全做出的努力,那我们开发者有没有一个可以贡献的地方呢?

显然是有的,我们的常用到的手机,有手势密码,有数字密码,有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.

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.