两分钟实现安全完备的登录模块
引言
2016年中,我所在的项目组将原来系统中的登录模块拆出来做成一套集中账号管理系统,并对外提供单点登录的服务。后来,公司中需要使用员工账号进行登录的系统越来越多,但这些系统都是各有各的实现方式,管理比较混乱。为了推广我们组的账号管理系统,统一公司的账号体系,我写了一篇“软文”希望在公司技术月刊上发表,便是这篇文章的来历。
随着公司业务的不断发展,各种内部管理系统也越来越多,这些系统虽然功能职责各不相同,但有一个功能模块是所有这些系统都必备的,那就是登录模块。登录模块负责生成并存储识别用户身份的数据,在有用户对系统进行操作的时候验明用户的真实身份,并进行适当的权限限制。
如何实现登录模块
要实现一个基础的登录模块非常简单,大概有下面几个步骤:
- 实现用户注册页面,用户填写包括用户名密码的基本信息之后,将用户信息存储到数据库。
- 用户尝试对系统进行操作前,判断用户是否登录,如果没有就跳转到登录页面让用户输入用户名密码。
- 用户输入正确的用户名密码之后,验明用户身份并生成 session 和 cookie,以便在用户做下一次操作时判断用户的身份和权限。
完成上述三个步骤,一个基本的用户登录模块就完成了。但是,凡事总有但是,事情并没有这么简单。在一个真实的场景中,作为系统信息安全的重要保障,登录模块往往还需要考虑很多的实现细节。
弱口令限制
很多用户在注册的时候,喜欢使用非常简单的密码,比如 123456 或者 qweasd之类的。尤其在企业内部系统中,这种用户密码更是常见。但是,这类密码简直形同虚设,如此简单的用户密码,非常容易被恶意用户猜解,从而导致信息的泄露,对企业信息安全造成严重威胁。为了避免这种情况的发生,我们在新用户注册或用户重设密码的时候,往往会对用户密码复杂度做出一定的限制,比如要求用户密码必须同时包含数字、大写字母、小写字母、特殊符号四类字符中的至少三类,且密码位数不能低于8位。
注册验证
作为一个企业内部系统,我们往往不希望企业外部人员也能够注册成为用户,因为这很可能会导致企业内部信息的泄露。那么,如何保证外部人员无法注册呢?常见的手段是使用企业邮箱对注册人的身份进行验证。比如,用户在注册时,除了要用户输入用户名和密码以外,还需要用户输入其企业邮箱账号。用户提交注册表单后,系统验证其输入的邮箱账号是企业内部的合法邮箱账号,并向这个邮箱发一封注册验证邮件,其内包含一个随机生成的复杂链接,用户点击这个链接之后,才能完全注册成功。
加密存储密码
用户注册成功之后,系统需要将用户名和密码的相关信息写入数据库或类似介质,以便用户下次登录系统时查询验证。可是为了防备一些非常规的情况,我们往往不能直接明文存储用户的密码,而是用摘要算法对用户密码进行若干处理,再将摘要信息写入数据库中。比如,用户的原始密码是 hell0W0rld,我们先用 MD5 算法对密码做摘要,得到摘要密文 17529cb075a386f08409f260bf0dfb8c,然后再用用户的注册时间戳作为 salt 拼接到密文上,得到 17529cb075a386f08409f260bf0dfb8c1484547629852,再对这个字符串用 MD5 算法做摘要,得到密码摘要信息 6ee203a6a6c05a6f8f958c5be00b1313,最后我们将用户名、密码摘要信息、用户注册时间戳三个信息全部写入数据库中,以便将来验证用户身份。
这样的做法虽然看起来比较繁琐,但其安全性得到了比较高的保障。即使数据库中的密码信息被完全盗取,且用户的原始密码相对简单,面对加了随机salt且做过两次摘要的密码信息,想要猜解出原始密码也是非常困难的
验证码
暴力猜解是比较常见的用户信息猜解手段,简单点说,就是用程序反复尝试用不同的密码登录某个账户,直到成功为止。这种破解用户密码的方式,一方面增加了用户信息泄露的风险,另一方面也由于系统需要不断的查询数据库验证密码正确性,而大大增加了系统的资源消耗。为了防止这种情况,一般我们需要通过随机图片验证码来验证当前提交登录请求的是人而不是某种程序。而一般为了平衡用户体验和系统安全性两方面的要求,常见的做法是用户输错密码3次后再要求用户输入验证码。
失败次数
虽然我们加了图片验证码提高系统的安全性,但还是有一些比较高级的破解程序,可以正确识别出图片验证码的内容。所以,我们还需要加上尝试失败次数的限制,当用户尝试登录失败超过指定的次数之后,系统就会锁定该账号。一段时间内,无论用户是否输入正确的用户密码,系统都拒绝该账号的登录请求,必须找系统管理员手段解锁,或等待一段时间之后才能再次尝试。
找回密码
找回密码也是很常见的需求点。用户有时候真的会忘记自己的密码,这时,需要用户能够手动重置自己的密码。常见的做法是用户提交重置密码请求,系统向该用户的注册邮箱发一封密码重置邮件,内含一个随机生成的复杂链接,用户通过这个链接地址访问系统的密码重置页面来重置自己的密码。
HTTPS
用户在发起登录请求时,输入的账号和密码信息通过网络发往服务器,这些信息在发往服务器的过程中会经历非常多的网络节点,如果信息在传输过程中是明文状态,那么这些网络节点就都可以获取到用户的账号和密码信息,这将成为非常大的系统信息安全隐患。因此,对用户登录过程中的敏感信息进行加密传输是非常必要的。最常见的做法就是对登录请求用 HTTPS 协议代替 HTTP 协议。
跨平台特性
很多的系统都具有跨平台特性,比如 OA 系统,我们可以在 PC 上填写加班申请单,在手机浏览器上查看这个申请单的审批状态,而如果公司员工是在微信中访问 OA 系统的页面,OA 系统还会通过关联的微信账号让用户直接登录系统,免去了输入用户名密码的步骤,从而大大提高了用户操作的便利性。
由此我们可以看出,要实现一个功能完备又安全的登录模块,并不是一件非常简单的事情,这其中设计、开发、测试的工作都需要花费大量的时间和人力,那么我们又怎么能在两分钟内为各应用系统实现这么多复杂的功能呢?答案就是使用 UserCenter。
如何使用 UserCenter
UserCenter 是 OA 组根据公司实际业务场景设计实现的单点登录系统,功能完善,可靠性高,使用方便。
使用 UserCenter 只需简单的两个步骤:
安装 UserCenter SDK
UserCenter 通过 Composer 包分发其 SDK,只要在项目的 Composer 配置中声明依赖 UserCenter 就可以了。
- 修改 Controller 基类的 _initialize 方法,调用 UserCenter 的接口
复制上面的代码到 Controller 基类中,修改第 4 行和第 15 行的代码即可。
UserCenter 的其他优势
UserCenter 系统不仅功能完备安全性高,可靠性也值得信赖。目前系统采用主从备份部署方式,武汉和深圳机房各有一套服务处于运行状态。员工账号数据能够实时在主从服务间进行同步,一旦主服务因网络或电力故障无法访问,系统会将登录和身份验证的相关网络请求切换到备用服务上,使得各应用系统的用户登录功能不受影响。
同时,UserCenter 还支持各种第三方系统通过 LDAP 接口接入,如公司正在使用 PMS、WIKI 以及 JENKINS 系统等等。
另外,使用 UserCenter 之后,公司各内部系统共用同一套账号系统,在新员工入职和老员工离职时,账号的创建和回收工作都将更加便利和安全。同时,由于使用 UserCenter 的各个应用系统不再需要单独保存用户的账号和密码信息,使得公司员工账号泄露的风险大大降低。
结语
目前,公司还没有统一的的身份验证策略和框架。随着业务的增长和时间的推移,这将导致大量内部系统都拥有一套自己的身份验证模块和用户账号数据。每个员工都需要记住多个用户名和密码,才能访问各个不同的系统。这给系统管理人员以及公司的各个员工都带来很大的负担。如果没有统一的策略,开发人员就需要为每个系统重复实现定制的登录模块,这还会导致各种维护上的麻烦问题。UserCenter 为安全性和身份验证提供了统一的范式,大大减轻了用户、管理员和开发人员的负担。