精华 密码保存的最佳实践讨论
发布于 3个月前 作者 russj 867 次浏览 来自 分享

最近总结了一下密码保存的方法,原文在这里 http://jsnoder.com/blog/mi-ma-bao-cun/. 希望有更多经验的人来说说看法,使大家少犯低级错误。


网络应用开发,或多或少都会涉及到用户密码的保存。这里主要介绍一下密码保存的基本常识。希望给平时不太接触这些的人提供一点参考。

明文保存

新手最容易犯的错误就是保存明文密码。

所谓的明文就是不经过任何加密,用户输入什么就在自己服务器的数据库里保存什么。这种错误发生过很多,比较有名的是 CSDN 明文口令泄露事件。这样做的最大坏处是很多用户喜欢使用同样的用户名,比如自己的email 地址,和同样的密码来登录不同的网站。这样一旦泄露,用户的其他网络账号就受到威胁,不得不更改密码。

不过,谷歌的 Chrome 是用明文来保存用户在各个网站的密码。如果你让 Chrome 记住你的密码,之后你就可以在 Chrome 的设置->显示高级设置->密码和表格 里查看到自己的密码。 不过也不是完全的明文,谷歌也是使用了加密算法的,但是是可以逆转的加密。只要有系统的密码就能查看原始密码。

谷歌当然不会犯低级错误,他们的想法是如果用户的电脑已经被黑客拿到,并且管理员密码被破解,已经不安全了,无法阻止其他口令被盗。而且 Chrome 要保存别的网站密码的话也只有使用明文或者叫密文来实现。最后让用户自己在方便性和安全上权衡利弊。
还有就是对于谷歌来说,即使被破解了,也是单个用户受影响。网络服务就不同了,被人爆了库后成千上万的用户受影响,公司被告倒闭都有可能,而且还要防止内部监守自盗。 我想到一个比喻。网站的密码就像银行的钱一样,一旦银行被黑就是成千上万客户的钱被盗。Chrome 保存的密码就像大家钱包里的钱,钱包都被偷了,钱还安全吗?钱包被偷也就是单个用户受损,而且责任也是用户自己没有看好钱包。所以虽然引来安全专家们的一片哗然,谷歌还是继续我行我素,毕竟是用户导向的产品,好的用户体验更重要。 扯远了。

所以一定不要保存用户的可逆密码。可以使用下面的方法来代替:

哈希

生成一个哈希值来做对照。用户每次登陆时,使用同样的算法对用户输入的密码哈希,生成的值与存在数据库里的作对比。这样就不用保存原始密码了,别人即使盗去哈希值也没用。

简单来说加密是可逆的,就是解密后能够还原原始密码;而哈希是多对一的关系,无穷多的字符串可能生成同样的哈希值,所以就算黑客取得了哈希后的字符串也没有办法还原最初的密码。关于哈希(hash)和加密(encrypt)的区别,参考 这里

不够话说道高一尺,魔高一丈,黑客们还有其他的攻击方法。比如使用字典攻击,彩虹表攻击。基本原理就是使用事先计算好的哈希值来暴力破解。据说能下载到的彩虹表有上 100GB 大。要想不被猜到密码就要使用长的无规律字符串,像 ‘123456’ 这样的密码能被前述的方式轻易破解。

要想加大黑客破解的难度,就得:

加盐哈希

使用秘钥或者插入随机产生的字符串(密码学里被称为盐 salt)。这样虽然加大了破解难度,但是厉害的黑客还是能够破解,而且现在计算机越来越快,意味着穷举破解的速度也越来越快。

最后的解决方案:

密码哈希算法(Password Hashing Algorithm)

常用的有两种:pdkdf2 和 bcrypt。

两者都使用一种叫秘钥延伸(Key stretching)的技术。简单来说,这种技术让哈希算法变得足够慢,而且可以人为的调得更慢,使得暴力破解更加困难。详见 这里.

所以在 Node.js 里很多用户验证框架都使用 bcrypt 来加密哈希密码。

最后,黑客们还有一种方式,被称为社会工程学。其中一点就是利用人们常用的设置密码方式来排列组合猜测密码。以上所有的技术都难挡这种方法。 据说前段时间苹果的 iCloud 女明星裸照泄露事件的起因就是美女们的密码被人破解。黑客先利用‘Find My iPhone’ app 可以无限次试密码的漏洞,从常用弱密码里枚举出密码,然后登录明星们的账号后下载了存在云服务器里的裸照。后来苹果限制到只能五次错误,堵上了这个漏洞。 到 Github 这里 可以下载到这次黑客攻击使用的源码。不知真假,不过加星过千了。

所以另外一个安全方法就是,如果同一账号短时间内输入密码错误次数太多,那么就临时锁死这个账号,不让再次试错。

6 回复
UserSchema.pre('save', function(next) {
    var user = this;

    // only hash the password if it has been modified (or is new)
    if (!user.isModified('password')) return next();

    // generate a salt
    bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
        if (err) return next(err);

        // hash the password using our new salt
        bcrypt.hash(user.password, salt, function (err, hash) {
            if (err) return next(err);

            // set the hashed password back on our user document
            user.password = hash;
            next();
        });
    });
});

@i5ting 这个salt没有保存的话,怎么验证密码呢? 这是一段来自MEAN.JS的代码。salt值也被保存了。

/**
 * Hook a pre save method to hash the password
 */
UserSchema.pre('save', function(next) {
    if (this.password && this.password.length > 6) {
        this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64');
        this.password = this.hashPassword(this.password);
    }

    next();
});

/**
 * Create instance method for hashing a password
 */
UserSchema.methods.hashPassword = function(password) {
    if (this.salt && password) {
        return crypto.pbkdf2Sync(password, this.salt, 10000, 64).toString('base64');
    } else {
        return password;
    }
};

/**
 * Create instance method for authenticating user
 */
UserSchema.methods.authenticate = function(password) {
    return this.password === this.hashPassword(password);
};

采用2进制+独立账户关键KEY 混编加密好了。不用哈希也安全。

@MiguelValentine 有详细点的文章吗?

回到顶部