标题:什么叫给密码“加盐”?如何安全的为你的用户密码“加盐”?

文章目录
    分类:PHP 标签:PHP基础,加解密

    什么叫给密码“加盐”?如何安全的为你的用户密码“加盐”?

    在面对这个网络世界的时候,密码安全总是各个公司和用户都非常关心的一个内容,毕竟现在大家不管是休闲娱乐还是学习购物都是通过网上的帐号来进行消费的,所以我们通常会给用户的密码进行加密。在加密的时候,经常会听到“加盐”这个词,这是什么意思呢?


    我们通常会将用户的密码进行 Hash 加密,如果不加盐,即使是两层的 md5 都有可能通过彩虹表的方式进行破译。彩虹表就是在网上搜集的各种字符组合的 Hash 加密结果。而加盐,就是人为的通过一组随机字符与用户原密码的组合形成一个新的字符,从而增加破译的难度。就像做饭一样,加点盐味道会更好。


    接下来,我们通过代码来演示一种比较安全的加盐方式。


    首先,我们建一个简单的用户表。这个表里只有四个字段,在这里仅作为测试使用。

    CREATE TABLE `zyblog_test_user` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `username` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
        `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
        `salt` char(4) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '盐',
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

    然后定义两个方式,一个用来生成盐,一个用来生成加盐后的 Hash 密码。

    /**
     * 随机生成四位字符串的salt
     * 也可以根据实际情况使用6位或更长的salt
     */
    function generateSalt()
    {
        // 使用随机方式生成一个四位字符
        $chars = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
        for ($i = 0; $i < 4; $i++) {
            $str .= $chars[mt_rand(0, count($chars) - 1)];
        }
        return $str;
    }
    
    /**
     * 密码生成
     * 使用两层hash,将salt加在第二层
     * sha1后再加salt然后再md5
     */
    function generateHashPassword($password, $salt)
    {
        return md5(sha1($password) . $salt);
    }

    generateSalt() 方法很简单,就是生成一个随机的四位字符的字符串,我们使用大小写加数字的形式生成这个字符串。这就是传说中的“盐”。


    接下来我们就可以使用 generateHashPassword() 方法为用户的原密码加盐。在这里我们第一层先使用 sha1() 对原密码进行一次 Hash ,然后使用这个 Hash 值拼接盐字符串后再进行 md5() 加密。最后加密出来的 Hash 值就很难在彩虹表中找到了。即使找到,也只是上层 sha1() 拼接盐字符串的内容,用户的原文密码毕竟还有一层加密。


    剩下的就是我们进行出入库的注册登录测试了。

    $pdo = new PDO('mysql:host=localhost;dbname=blog_test;charset=utf8mb4', 'root', '');
    
    $username = 'ZyBlog1';
    $password = '123456';
    
    // 注册
    function register($username, $password)
    {
        global $pdo;
    
        // 首先判断用户是否已注册
        $pre = $pdo->prepare("SELECT COUNT(id) FROM zyblog_test_user WHERE username = :username");
        $pre->bindParam(':username', $username);
        $pre->execute();
        $result = $pre->fetchColumn();
    
        // 如果用户名存在,则无法注册
        if ($result > 0) {
            echo '用户名已注册!', PHP_EOL;
            return 0;
        }
    
        // 生成salt
        $salt = generateSalt();
        // 密码进行加盐hash处理
        $password = generateHashPassword($password, $salt);
    
        // 插入新用户
        $pre = $pdo->prepare("insert into zyblog_test_user(username, password, salt) values(?, ?, ?)");
    
        $pre->bindValue(1, $username);
        $pre->bindValue(2, $password);
        $pre->bindValue(3, $salt);
    
        $pre->execute();
    
        return $pdo->lastInsertId();
    }
    
    $userId = register($username, $password);
    if ($userId > 0) {
        echo '注册成功!用户ID为:' . $userId, PHP_EOL;
    }
    
    // 注册成功!用户ID为:1
    
    // 查询数据库中的数据
    $sth = $pdo->prepare("SELECT * FROM zyblog_test_user");
    $sth->execute();
    
    $result = $sth->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);
    
    // Array
    // (
    //     [0] => Array
    //         (
    //             [id] => 1
    //             [username] => ZyBlog1
    //             [password] => bbff8283d0f90625015256b742b0e694
    //             [salt] => xOkb
    //         )
    
    // )
    
    // 登录时验证
    function login($username, $password)
    {
        global $pdo;
        // 先根据用户名查表
        $pre = $pdo->prepare("SELECT * FROM zyblog_test_user WHERE username = :username");
        $pre->bindParam(':username', $username);
        $pre->execute();
        $result = $pre->fetch(PDO::FETCH_ASSOC);
    
        // 用户名存在并获得用户信息后
        if ($result) {
            // 根据用户表中的salt字段生成hash密码
            $password = generateHashPassword($password, $result['salt']);
    
            // 比对hash密码确认登录是否成功
            if ($password == $result['password']) {
                return true;
            }
        }
        return false;
    }
    
    $isLogin = login($username, $password);
    if ($isLogin) {
        echo '登录成功!', PHP_EOL;
    } else {
        echo '登录失败,用户名或密码错误!', PHP_EOL;
    }
    
    // 登录成功!

    代码还是比较简单的,在注册的时候,我们直接对用户密码进行加密后入库。主要关注的地方是在登录时,我们先根据用户名查找出对应的用户信息。然后将用户登录提交上来的原文密码进行加密,与数据库中的原文密码进行对比验证,密码验证成功即可判断用户登录成功。


    另外还需要注意的是,我们的盐字符串也是要存到数据库中的。毕竟在登录的时候我们还是需要将用户的原文密码与这个盐字符串进行组合加密之后才能进行密码的匹配。


    这样加密后的代码其实想通过彩虹表来破解基本上是很难了。在几年前 CSDN 的帐号泄露事件中,大家发现作为中文程序员世界最大的网站竟然是明文存储的密码,这就为攻击者提供了一大堆用户的明文常用密码。因为大家都喜欢用同一个用户名和密码注册不同的网站,所以不管其他怎么加盐都是没用的,毕竟原文密码是对的,拿到这样一个网站的数据库中的用户明文密码后,就可以通过这些密码去尝试这些用户在其他网站是不是用了相同的帐号名和密码注册了帐号。所以在日常生活中,我们重要的一些网站帐号、密码尽量还是使用不同的内容,如果记不住的话,可以使用一些带加密能力的记事本软件进行保存,这样会更加安全。而我们程序员,则应该始终都将用户的密码及重要信息进行加密处理,这是一种基本的职业规范。


    测试代码:


    https://github.com/zhangyue0503/dev-blog/blob/master/php/202003/source/%E4%BB%80%E4%B9%88%E5%8F%AB%E7%BB%99%E5%AF%86%E7%A0%81%E2%80%9C%E5%8A%A0%E7%9B%90%E2%80%9D%EF%BC%9F%E5%A6%82%E4%BD%95%E5%AE%89%E5%85%A8%E7%9A%84%E4%B8%BA%E4%BD%A0%E7%9A%84%E7%94%A8%E6%88%B7%E5%AF%86%E7%A0%81%E2%80%9C%E5%8A%A0%E7%9B%90%E2%80%9D%EF%BC%9F.php

    视频链接

    B站视频地址:https://www.bilibili.com/video/BV1Ev411b7UR

    微信视频地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&amp;mid=2247485020&amp;idx=1&amp;sn=618251625e2dbafceb2fcc30273f0d22&amp;chksm=97ebf7fda09c7eeb60492107b3032489e4ee4be19d2791af90ae0f8326de4b1952ff546e36c3&amp;scene=27#wechat_redirect

    微信文章地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&amp;mid=2247484554&amp;idx=1&amp;sn=134a878c30fb702cf5fcb06fef3f16dd&amp;chksm=97ebf52ba09c7c3d4000bae8cb62b90f7f759e019006437cdf5349dfa45ea4bf3c2fb0184d86&amp;scene=27#wechat_redirect

    搜索
    关注