0%

如何设计一款安全高可用的登录模块

补作业

背景

登录模块,是网站用户使用的第一个入口,也是最基本的功能,关系到用户数据和应用系统数据的安全。登录入口也是通向服务器的一个关键地方,如果登录入口设计的不够安全,那么整个系统将面临着致命安全隐患。这里我主要介绍下在前后端分离架构下,网页应用的登录模块的安全设计与具体的方案实现介绍。

概念

本文介绍的是前后端分离架构的系统,所以安全角度分为客户端(前端)和服务端。
客户端的安全,主要是用户密码本身的安全性(密码长度和复杂性等)以及用户电脑的安全性,包括用户电脑没有安装黑客木马软件,登录程序没有被第三方程序加载调试,用户录入框组织键盘Hook程序等等,通过一些代码即可解决。

服务器端的安全,包括服务器自身的安全(系统漏洞等等)以及程序设计上的安全,这里主要讲一下程序设计上的安全。最基本的问题是,用户的密码不应该直接保存在服务器的数据库上,也不应该将密码用单钥算法加密后保存。目前大多数网站都使用MD5函数进行登录认证,不过我推荐使用安全性更高的SHA1散列函数来进行登录认证。

安全防护方案

启用https

HTTP请求都是明文传输的,所谓的明文指的是没有经过加密的信息,如果HTTP请求被黑客拦截或嗅探到,并且里面含有银行卡密码等敏感数据的话,会非常危险。HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。数据是被对称加密传输的,对称加密过程需要客户端的一个密钥,为了确保能把该密钥安全传输到服务器端,采用非对称加密对该密钥进行加密传输,总的来说,对数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。

登录高频限制

防止登录数据被通过接口进行高频的暴力猜解,或者防止某些IP恶意高频访问服务器,对服务器资源进行占用攻击,可以对这些IP进行限制,进行拦截。常用方式在应用服务器中进行标记记录,并结合实际的使用场景进行业务层的逻辑限制,或者直接通过Nginx本身的ip访问频率设置进行限制。

密码二次加密

很多人对于用户的原始密码安全,还停留在不被非法第三方获取的层面上,但实际上,原始 密码的最大威胁,往往来自于系统的开发人员和服务器的管理人员。这些人可能是有意收集,也可能是无意泄露,往往是用户原始密码的泄露的罪魁祸首。在构建登 录系统的时候,应该从根本上避免,做到只有用户自己和键盘记录器才知道原始密码。
因此我们分为两个部分去做:

  • 传输的密码加密处理
  • 入库的密码加密存储

    传输的密码加密处理

    前面我们开启了https后,所有的传输数据会做加密处理,但对于比较重要的数据,我们应该采取二次加密的方式加以保护。加密算法可以选择des、sha256、sha512、md5等结合约定的密钥来加密传输和解密。

下面是以python + des为例的加密、解密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pyDes import des, CBC, PAD_PKCS5

def des_encrypt(s):
"""
DES 加密
:param s: 原始字符串
:return: 加密后字符串,16进制
"""
secret_key = app.config['KEY']
iv = secret_key
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
en = k.encrypt(s, padmode=PAD_PKCS5)
return binascii.b2a_hex(en)

def des_descrypt(s):
"""
DES 解密
:param s: 加密后的字符串,16进制
:return: 解密后的字符串
"""
secret_key = app.config['KEY']
iv = secret_key
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
de = k.decrypt(binascii.a2b_hex(s), padmode=PAD_PKCS5)
try:
deStr = str(de,encoding='utf-8')
except:
deStr = de.encode("utf-8")
return deStr

入库的密码加密存储

入库的密码不能直接以明文形式存储,以防拖库后用户信息的被盗用(很多人习惯多个网站一个密码)。
这里我们可以通过hash算法,结合随机生成的salt给密码加密,每次调用密码校验是,在用用户输入的密码结合入库时的salt再次进行hash,来对比加密结果,以校验密码的正确性。
以python为例,获取加密密码和salt方法如下:

1
2
3
4
5
6
7
8
import os,hashlib,re

def encrypt_password(password, salt=None, encryptlop=30):
if not salt:
salt = os.urandom(16).encode('hex') # length 32
for i in range(encryptlop):
password = hashlib.sha256(password + salt).hexdigest() # length 64
return password, salt

验证码校验

为防止机器人登录或其它非正常手段,我们需要给登录功能加上验证码校验,验证码类型也有很多,交互方式和安全级别各有不同。验证码是后台随机产生的一个短暂的验证码,这个验证码一般是一个计算机很难识别的图片。这样就可以防止以程序的方式来尝试用户的口令。事实证明,这是最简单也最有效的方式。当然,总是让用户输入那些肉眼都看不清的验证码的用户体验不好,所以,可以折中一下。比如Google,当他发现一个IP地址发出大量的搜索后,其会要求你输入验证码。当他发现同一个IP注册了3个以上的gmail邮箱后,他需要给你发短信方式或是电话方式的验证码。

  • 数字图形验证码:数字加上干扰线,防止计算机能够轻易识别,这样也可以防止黑客以程序的方式来尝试登录。
  • 第三方动态图形识别:需要用户去识别并拖动校验,同类的还有图像内容识别点击,这种一般是用第三方集成好的sdk。
  • 手机短信验证码:依赖完善的用户信息,将登录行为与用户强关联,同时需注意保护用户的手机号隐私与防止短信滥用。
  • 邮件验证码:低成本的关联校验,依赖完善的用户信息,但操作链过于冗长。

无论何种形式的验证码,都要关注到验证码自身的安全问题(不可猜解、时效性、与帐号的关联性等)

下面以python为例生成图形验证码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import base64
import io
import random
import string
from PIL import Image, ImageFont, ImageDraw

class CaptchaTool(object):
"""
生成图片验证码
"""

def __init__(self, width=50, height=12):

self.width = width
self.height = height
# 新图片对象
self.im = Image.new('RGB', (width, height), 'white')
# 字体
self.font = ImageFont.load_default()
# draw对象
self.draw = ImageDraw.Draw(self.im)

def draw_lines(self, num=3):
"""
划线
"""
for num in range(num):
x1 = random.randint(0, self.width / 2)
y1 = random.randint(0, self.height / 2)
x2 = random.randint(0, self.width)
y2 = random.randint(self.height / 2, self.height)
self.draw.line(((x1, y1), (x2, y2)), fill='black', width=1)

def get_verify_code(self):
"""
生成验证码图形
"""
# 设置随机4位数字验证码
code = ''.join(random.sample(string.digits, 4))
# 绘制字符串
for item in range(4):
self.draw.text((6 + random.randint(-3, 3) + 10 * item, 2 + random.randint(-2, 2)),
text=code[item],
fill=(random.randint(32, 127),
random.randint(32, 127),
random.randint(32, 127))
, font=self.font)
# 划线
self.draw_lines()
# 重新设置图片大小
self.im = self.im.resize((100, 24))
# 图片转为base64字符串
buffered = io.BytesIO()
self.im.save(buffered, format="JPEG")
img_str = b"data:image/png;base64," + base64.b64encode(buffered.getvalue())
return img_str, code

cookie过期时间设置

由于http协议是无状态的,传统服务器只能被动响应请求,当服务器获取到请求,并为了能够区分每一个客户端,需要客户端发送请求时发送一个标识符(cookie),也因此为了提供这个标识符,产生了cookie技术,我们在请求头(Request Headers)中添加了标识符(cookie). cookie就是浏览器储存在用户电脑上的一小段文本文件,是纯文本格式,每次发送请求,都会把这个cookie随同其它报文一起发送给服务器。Web 页面或服务器告知浏览器按照一定规范来储存这些信息,并在随后的请求中将这些信息发送至服务器,Web 服务器就可以使用这些信息来识别不同的用户。大多数需要登录的网站在用户验证成功之后都会设置一个 cookie,只要这个 cookie 存在并可以,用户就可以自由浏览这个网站的任意页面。cookie 只包含数据,就其本身而言并不有害。

综上可知,cookie可以方便我们登录后登录身份的,但设想下如果我们长时间的离开操作位置,别人看到你的页面,如果当前有重要的操作可以点击触发,此时你还是登录状态,这样的话,你的数据就会被非法操作了。而此时由于cookie过期,需要用户重新登录才能继续之前的操作的话,一定程度上就保护了用户的重要数据。

实际应用过程中,我们还需要结合CSRF、SSRF等跨站攻击手段来合理的设置我们的cookie,以提高用户权限的安全性。

弱口令检测

弱口令漏洞指系统口令的长度太短或者复杂度不够,如仅包含数字或字母等。在安全方面弱口令问题算是技术含量最低的安全隐患了。但往往技术含量越低,被利用频率也越高,而且造成的影响还不见得小。弱口令容易被破解,一旦被攻击者获取,可用来直接登录系统,读取甚至修改网站代码。所以治理弱口令将成为安全体系建设中性价比最高的一个环节。但是就是这样一个性价比高的环节想完全杜绝却不是那么容易,未来的系统可以通过注册申请环节强制设置强密码,但是针对过去的老旧系统弱口令问题就令人头疼了。
因此在用户账号创建之初,就应当从前端以及后端加强对弱设置的检测。
下面以javascript中结合正则校验弱口令为例:

1
2
3
4
5
6
7
8
//强弱密码校验,密码至少包含大写字母,小写字母,数字,且不少于8位
var tx = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/;
var pass = 'aaaa12345'
if(!tx.test(pass)){
alert("弱密码");
}else{
alert("强密码")
}

sql预编译

执行登录校验,我们免不了,对于用户传输来的账号密码进行sql组装,进行正确性查询。但这个过程如果直接拼接sql参数很有可能就会出现’sql注入’等严重的安全问题。这种方式能防范SQL注入的原理是在SQL参数未注入之前,提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一条一个参数。
下面以PHP中MySQL的预编译为例:

1
2
3
4
5
6
7
8
9
/*1.假定已经完成数据库初始化操作,数据库对象名:$pdo*/
$sql = "insert into team values (null, :test_name)";
$PDOStatement = $pdo->prepare($sql);

/*2.绑定数据到中间编译结果*/
$PDOStatement->bindValue(':test_name','测试');

/*3.执行*/
$result = $PDOStatement->execute();

第三方登录

第三方登陆又叫统一认证,英文缩写叫SSO,早期是用在操作系统上的,比如LDAP等。统一认证的优势就是我只需要一个账号密码就可以维护大量不同的系统,甚至不同厂商开发的系统。如果没有这个,大概率也是所有的系统同一个人会使用同样的账号和密码,为了登陆方便,大概率会使用弱密码。而统一认证以后,用户只需要维护一个账号即可,这样用户大概率会设定一个稍微复杂一些的强密码。那么账号的安全性就得到了保障,所以统一认证是未来的一种趋势。

国内大型的互联网平台都有提供登录授权服务,但其会对接入的网站做层层限制,能到的用户信息,和用户登录时需要提供的信息都很少,把接口的安全问题抛给三方统一处理,这不仅仅是提高了登录的安全性,在一定程度上也提高登录模块的操作易用性。

结语

在Internet的飞速发展的今天,人们的工作和日常生活已离不开internet,比如网上购物,网上支付等。正所谓道高一尺魔高一丈,与之一同发展起来的web安全性问题,每天都在发生的黑客入侵及篡改网页,帐号窃取等问题越来越引起了人们的关注,因为随着Web内容的增加、应用程序功能的丰富和用户的普及,安全问题已经不容忽视。看似非常简单的登录模块,其实里面也存在着非常多的安全隐患。这些也是把好一个系统安全性的第一关,以上就是本人在应对项目安全的检查中所提出的解决方案。