laravel加密手机号以后手机号如何做登录校验

着实是想不通了,所以来社区请教一下各位大佬,
laravel的加密同一串字符串会发生变化,因为向量iv是随机值
目前我这的情况是使用手机号作为账号登录,手机号又需要加密存进数据库,那我用户校验登录的时候应该怎么校验手机号呢?
如果是新加一个字段用来存手机号的哈希,那是不是也不能用laravel的hash而是要自己重新写一个。

《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
DogLoML
最佳答案

有很多办法都可以实现,比如加个字段,加盐hash用于查找,我用的比较简单的一种,就是把encrypt加密函数中的随机iv改为确定值,可以根据模型id、手机号或者别的什么字符串来生成iv。

给User模型use一下我整理好的EncryptPhone就可以了 ,里面复制了encryptString方法,并把随机iv改为自定义iv

Laravel

通过模型修改器访问器自动加密解密

Laravel

Laravel

使用phoneIs的scope来查找用户,因为去掉了iv的随机性,phone加密后的值不变,直接where即可

例如:User::phoneIs('12345678901')->first()

Laravel

代码复制过去改改就能用:

<?php
/**
 * @Desc 手机号加密保存
 */

namespace Yang\Traits\Model;

use Exception;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
use Yang\Modules\Libs\DiyEncrypter;
use Illuminate\Database\Eloquent\Builder;

/**
 * @method static Builder|static phoneIs(string $phone)
 */
trait EncryptPhone {

    // 自定义你的iv生成方式,不要和我一样
    public static function getIv($value, $ivLen) {
        // 限定纯数字字符串,防止hex2bin奇数长度报错
        if(!is_numeric($value)){
            throw new Exception('必须为纯数字组成的字符串');
        }

        // 将手机号打乱顺序转为16位
        $value = preg_replace('/(\d{2})(\d{2})(\d{2})(\d)(\d{2})(\d{2})/', '$3$6$2$1$4$3$2$4$5', $value); // 将手机号转为16位

        // 生成固定长度iv
        $ivSource = str_split(substr(str_pad($value, $ivLen, '0', STR_PAD_LEFT), 0, $ivLen));
        $ivArr = [];
        // 第一位和最后一位拼接,第二位和倒数第二位拼接,以此类推,组成$ivLen个元素的数组,每个都是0~99
        foreach ($ivSource as $k => $v) {
            $ivArr[$k] = $v . $ivSource[$ivLen - $k - 1];
        }

        $iv = '';
        foreach ($ivArr as $k => $v) {
            // 把$v转成2位16进制,也就是10到ff,即16~255,$k防止每位相同,范围取决于$ivLen为16还是32,取最大范围0~31,  最大偏移量255-99-32=124 ,此处 0~124 都行
            $hex = dechex($k + $v + 99);
            // hex2bin把十六进制字符串转为二进制字符串,要求$hex为偶数长度,上面已经约束了范围
            $iv .= hex2bin($hex);
        }
        return $iv;
    }


    // 手机号加密存库,改自同名函数,iv改为确定值而非随机值
    public static function encryptString($value)
    {
        $serialize = false;
        // 继承框架的Encrypter,把有些protected属性改为public,使其在此处有权限调用
        $encrypter=new DiyEncrypter(\Crypt::getFacadeRoot());
        $ivLen = openssl_cipher_iv_length(strtolower($encrypter->cipher));

        // 根据手机号生成iv
        $iv = self::getIv($value, $ivLen);

        $tag = '';

        $value = $encrypter->supportedCiphers[strtolower($encrypter->cipher)]['aead']
            ? \openssl_encrypt(
                $serialize ? serialize($value) : $value,
                strtolower($encrypter->cipher), $encrypter->key, 0, $iv, $tag
            )
            : \openssl_encrypt(
                $serialize ? serialize($value) : $value,
                strtolower($encrypter->cipher), $encrypter->key, 0, $iv
            );

        if ($value === false) {
            throw new EncryptException('Could not encrypt the data.');
        }

        $iv = base64_encode($iv);
        $tag = base64_encode($tag);

        $mac = $encrypter->supportedCiphers[strtolower($encrypter->cipher)]['aead']
            ? '' // For AEAD-algoritms, the tag / MAC is returned by openssl_encrypt...
            : $encrypter->hash($iv, $value);

        $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new EncryptException('Could not encrypt the data.');
        }

        return base64_encode($json);
    }


    public function setPhoneAttribute($v){
        /** @var $this Model */
        if ($v) {
            $this->attributes['phone'] = self::encryptString($v,$this->getKey());
        }
    }

    public function getPhoneAttribute($v){
        try {
            return Crypt::decryptString($v);
        } catch (Exception $e) {
            return null;
            // throw $e;
        }
    }

    public function scopePhoneIs(Builder $query, string $phone){
        return $query->where('phone', self::encryptString($phone));
    }
}

因为是复制的Encrypter,所有参数和系统的一样,所以可以直接用decrypt解密。

<?php
/**
 * @Desc 用来复制Encrypter,把protected改为public在User中才能使用
 */

namespace Yang\Modules\Libs;

use Illuminate\Encryption\Encrypter;
use Yang\Traits\CopyObjectAttributes;

class DiyEncrypter extends Encrypter {
    use CopyObjectAttributes;

    public $cipher;
    public $key;
    public $supportedCiphers = [
        'aes-128-cbc' => ['size' => 16, 'aead' => false],
        'aes-256-cbc' => ['size' => 32, 'aead' => false],
        'aes-128-gcm' => ['size' => 16, 'aead' => true],
        'aes-256-gcm' => ['size' => 32, 'aead' => true],
    ];
    public function hash($iv, $value)
    {
        return hash_hmac('sha256', $iv.$value, $this->key);
    }
}
<?php
/**
 * Desc: new对象时传一个对象进来,复制目标的属性,如果是继承关系且不希望用反射,可以复制非private属性,如果不是继承,则反射获取所有属性
 */

namespace Yang\Traits;

trait CopyObjectAttributes {
    public function __construct(object $obj, $useRef = false, ...$args) {
        //如果是继承,并且不想用反射,则直接复制,会丢失private属性
        if (!$useRef && $this instanceof $obj) {
            $this->copyAttributes($obj);
        } else {
            //不是继承关系,则用反射修改权限后再复制
            $this->copyAttributesByRef($obj);
        }

        //初始化方法
        if (method_exists(static::class, 'init')) {
            $this->init(...$args);
        }
    }

    function copyAttributes($obj) {
        $objValues = get_object_vars($obj); // return array of object values
        foreach ($objValues as $key => $value) {
            $this->$key = $value;
        }
    }

    function copyAttributesByRef($obj) {
        if (is_null($obj)) {
            return;
        }
        $ref = new \ReflectionClass(get_class($obj));
        // dd($ref->getProperties());
        foreach ($ref->getProperties() as $proRef) {
            $this->copyOneAttribute($proRef, $obj);
        }
    }

    public function copyOneAttribute(\ReflectionProperty $proRef, $obj) {
        if ($proRef->isProtected() || $proRef->isPrivate()) {
            $proRef->setAccessible(true);
        }
        if ($proRef->isStatic()) {
            $name = $proRef->getName();
            return static::${$name} = $proRef->getValue($obj);
        }
        return $this->{$proRef->getName()} = $proRef->getValue($obj);
    }

    public function init(...$args) {
    }
}

效果参考:

Laravel

补充:看到评论说固定iv的安全性问题,这个我不太了解,查了下确实存在一定的安全隐患,如果保密要求高的话建议不要使用我这种方法

chatgpt:

攻击者可以使用不同的方式对固定 IV 的加密算法进行攻击,以下是一些常见的攻击方式:

1.  唯一性攻击(Uniqueness Attack):攻击者可以利用固定 IV 导致加密数据非唯一的缺陷来破解加密数据。攻击者可以针对特定类型的明文和密码,通过破解少量的密文来还原秘密密钥以及所有的密文。

2.  重放攻击(Replay Attack):攻击者可以使用已知的固定 IV ,将之前的加密数据重新发送到服务器。由于使用相同的固定 IV,服务器会认为重放的密文是新的数据,从而解密并执行相应的操作。这可能导致安全风险。

3.  字符猜测攻击(Char Guessing Attack):攻击者知道使用的固定 IV ,可以利用这个信息来猜测明文以及密码。攻击者可以猜测出一个字符并检查加密数据的连续的一组字节是否在解密后正确。

4.  参数篡改攻击(Parameter Tampering Attack):攻击者可以利用固定 IV 将攻击代码嵌入到加密数据中,使得加密数据绕开入口验证(如管理员密码),并执行代码。由于 IV 是固定的,因此每次加密相同的数据时,生成的密文也是相同的。

因此,使用固定IV是不安全的,因为在攻击者拥有固定IV后,加密算法的弱点被发掘。最好的方法是使用随机 IV 来增加加密数据的安全性。
10个月前 评论
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
Rache1 10个月前
2daye 2周前
讨论数量: 35

哈希加密后的值会变化,但是调用验证方法 check 还是能验证通过的。 跟验证密码一个道理:

if (Hash::check('plain-text', $hashedPassword)) {
    // The passwords match...
}

注意:验证思路不是通过每次加密后的密文和数据库存储的密文进行比较的,而是通过 check 方法进行验证。

10个月前 评论
忆往昔弹指间 10个月前
快乐的皮拉夫 (作者) 10个月前
忆往昔弹指间 10个月前
wangtufly (楼主) 10个月前

只能查出用户数据比对手机号是不是这个用户的

10个月前 评论
wangtufly (楼主) 10个月前

来学习一下,感觉不管怎么做都很麻烦,感觉如果是我会在用户注册更高后 在redis里存一下手机号 用户id,加密向量,登录时根据手机号去,取这些的值,然后和数据库手机号对比 和数据库密码对比 应该可以实现你的需求 就是很麻烦 应该有更好的方法

10个月前 评论

手机号还是好的呢, 有的字段需要加密,还需要支持查询, 这种的不会呢

10个月前 评论

对称加密啊,比如md5

10个月前 评论
sanders

直接用 openssl_encryptopenssl_decrypt 进行加解密。

10个月前 评论

m.toutiao.com/is/Us6AEHA/ - 加密后的敏感字段还能进行模糊查询吗?该如何实现? - 今日头条

10个月前 评论
wangtufly (楼主) 10个月前
空巢搬砖仔 10个月前

除了 密文 外,再弄加一个对应的 hash 列,查询的时候把传入的手机号进行 hash ,然后去数据库根据 hash 拿到就行了。

另外,hash 时最好要加上个 slat,这个 slat 最好不要是固定值,可以与原文有关也行。

举个例子:

// 原文
$phone = post('phone');

// 这两个都存起来,下次查找的时候,根据 hash 去查找就好了
$phoneEncrypted = encrypt($phone);
// 后面部分主要目的时加 slat
$phoneHash = sha1($phone . base64_encode($phone) . md5($phone));
10个月前 评论
wangtufly (楼主) 10个月前
DogLoML

有很多办法都可以实现,比如加个字段,加盐hash用于查找,我用的比较简单的一种,就是把encrypt加密函数中的随机iv改为确定值,可以根据模型id、手机号或者别的什么字符串来生成iv。

给User模型use一下我整理好的EncryptPhone就可以了 ,里面复制了encryptString方法,并把随机iv改为自定义iv

Laravel

通过模型修改器访问器自动加密解密

Laravel

Laravel

使用phoneIs的scope来查找用户,因为去掉了iv的随机性,phone加密后的值不变,直接where即可

例如:User::phoneIs('12345678901')->first()

Laravel

代码复制过去改改就能用:

<?php
/**
 * @Desc 手机号加密保存
 */

namespace Yang\Traits\Model;

use Exception;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
use Yang\Modules\Libs\DiyEncrypter;
use Illuminate\Database\Eloquent\Builder;

/**
 * @method static Builder|static phoneIs(string $phone)
 */
trait EncryptPhone {

    // 自定义你的iv生成方式,不要和我一样
    public static function getIv($value, $ivLen) {
        // 限定纯数字字符串,防止hex2bin奇数长度报错
        if(!is_numeric($value)){
            throw new Exception('必须为纯数字组成的字符串');
        }

        // 将手机号打乱顺序转为16位
        $value = preg_replace('/(\d{2})(\d{2})(\d{2})(\d)(\d{2})(\d{2})/', '$3$6$2$1$4$3$2$4$5', $value); // 将手机号转为16位

        // 生成固定长度iv
        $ivSource = str_split(substr(str_pad($value, $ivLen, '0', STR_PAD_LEFT), 0, $ivLen));
        $ivArr = [];
        // 第一位和最后一位拼接,第二位和倒数第二位拼接,以此类推,组成$ivLen个元素的数组,每个都是0~99
        foreach ($ivSource as $k => $v) {
            $ivArr[$k] = $v . $ivSource[$ivLen - $k - 1];
        }

        $iv = '';
        foreach ($ivArr as $k => $v) {
            // 把$v转成2位16进制,也就是10到ff,即16~255,$k防止每位相同,范围取决于$ivLen为16还是32,取最大范围0~31,  最大偏移量255-99-32=124 ,此处 0~124 都行
            $hex = dechex($k + $v + 99);
            // hex2bin把十六进制字符串转为二进制字符串,要求$hex为偶数长度,上面已经约束了范围
            $iv .= hex2bin($hex);
        }
        return $iv;
    }


    // 手机号加密存库,改自同名函数,iv改为确定值而非随机值
    public static function encryptString($value)
    {
        $serialize = false;
        // 继承框架的Encrypter,把有些protected属性改为public,使其在此处有权限调用
        $encrypter=new DiyEncrypter(\Crypt::getFacadeRoot());
        $ivLen = openssl_cipher_iv_length(strtolower($encrypter->cipher));

        // 根据手机号生成iv
        $iv = self::getIv($value, $ivLen);

        $tag = '';

        $value = $encrypter->supportedCiphers[strtolower($encrypter->cipher)]['aead']
            ? \openssl_encrypt(
                $serialize ? serialize($value) : $value,
                strtolower($encrypter->cipher), $encrypter->key, 0, $iv, $tag
            )
            : \openssl_encrypt(
                $serialize ? serialize($value) : $value,
                strtolower($encrypter->cipher), $encrypter->key, 0, $iv
            );

        if ($value === false) {
            throw new EncryptException('Could not encrypt the data.');
        }

        $iv = base64_encode($iv);
        $tag = base64_encode($tag);

        $mac = $encrypter->supportedCiphers[strtolower($encrypter->cipher)]['aead']
            ? '' // For AEAD-algoritms, the tag / MAC is returned by openssl_encrypt...
            : $encrypter->hash($iv, $value);

        $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new EncryptException('Could not encrypt the data.');
        }

        return base64_encode($json);
    }


    public function setPhoneAttribute($v){
        /** @var $this Model */
        if ($v) {
            $this->attributes['phone'] = self::encryptString($v,$this->getKey());
        }
    }

    public function getPhoneAttribute($v){
        try {
            return Crypt::decryptString($v);
        } catch (Exception $e) {
            return null;
            // throw $e;
        }
    }

    public function scopePhoneIs(Builder $query, string $phone){
        return $query->where('phone', self::encryptString($phone));
    }
}

因为是复制的Encrypter,所有参数和系统的一样,所以可以直接用decrypt解密。

<?php
/**
 * @Desc 用来复制Encrypter,把protected改为public在User中才能使用
 */

namespace Yang\Modules\Libs;

use Illuminate\Encryption\Encrypter;
use Yang\Traits\CopyObjectAttributes;

class DiyEncrypter extends Encrypter {
    use CopyObjectAttributes;

    public $cipher;
    public $key;
    public $supportedCiphers = [
        'aes-128-cbc' => ['size' => 16, 'aead' => false],
        'aes-256-cbc' => ['size' => 32, 'aead' => false],
        'aes-128-gcm' => ['size' => 16, 'aead' => true],
        'aes-256-gcm' => ['size' => 32, 'aead' => true],
    ];
    public function hash($iv, $value)
    {
        return hash_hmac('sha256', $iv.$value, $this->key);
    }
}
<?php
/**
 * Desc: new对象时传一个对象进来,复制目标的属性,如果是继承关系且不希望用反射,可以复制非private属性,如果不是继承,则反射获取所有属性
 */

namespace Yang\Traits;

trait CopyObjectAttributes {
    public function __construct(object $obj, $useRef = false, ...$args) {
        //如果是继承,并且不想用反射,则直接复制,会丢失private属性
        if (!$useRef && $this instanceof $obj) {
            $this->copyAttributes($obj);
        } else {
            //不是继承关系,则用反射修改权限后再复制
            $this->copyAttributesByRef($obj);
        }

        //初始化方法
        if (method_exists(static::class, 'init')) {
            $this->init(...$args);
        }
    }

    function copyAttributes($obj) {
        $objValues = get_object_vars($obj); // return array of object values
        foreach ($objValues as $key => $value) {
            $this->$key = $value;
        }
    }

    function copyAttributesByRef($obj) {
        if (is_null($obj)) {
            return;
        }
        $ref = new \ReflectionClass(get_class($obj));
        // dd($ref->getProperties());
        foreach ($ref->getProperties() as $proRef) {
            $this->copyOneAttribute($proRef, $obj);
        }
    }

    public function copyOneAttribute(\ReflectionProperty $proRef, $obj) {
        if ($proRef->isProtected() || $proRef->isPrivate()) {
            $proRef->setAccessible(true);
        }
        if ($proRef->isStatic()) {
            $name = $proRef->getName();
            return static::${$name} = $proRef->getValue($obj);
        }
        return $this->{$proRef->getName()} = $proRef->getValue($obj);
    }

    public function init(...$args) {
    }
}

效果参考:

Laravel

补充:看到评论说固定iv的安全性问题,这个我不太了解,查了下确实存在一定的安全隐患,如果保密要求高的话建议不要使用我这种方法

chatgpt:

攻击者可以使用不同的方式对固定 IV 的加密算法进行攻击,以下是一些常见的攻击方式:

1.  唯一性攻击(Uniqueness Attack):攻击者可以利用固定 IV 导致加密数据非唯一的缺陷来破解加密数据。攻击者可以针对特定类型的明文和密码,通过破解少量的密文来还原秘密密钥以及所有的密文。

2.  重放攻击(Replay Attack):攻击者可以使用已知的固定 IV ,将之前的加密数据重新发送到服务器。由于使用相同的固定 IV,服务器会认为重放的密文是新的数据,从而解密并执行相应的操作。这可能导致安全风险。

3.  字符猜测攻击(Char Guessing Attack):攻击者知道使用的固定 IV ,可以利用这个信息来猜测明文以及密码。攻击者可以猜测出一个字符并检查加密数据的连续的一组字节是否在解密后正确。

4.  参数篡改攻击(Parameter Tampering Attack):攻击者可以利用固定 IV 将攻击代码嵌入到加密数据中,使得加密数据绕开入口验证(如管理员密码),并执行代码。由于 IV 是固定的,因此每次加密相同的数据时,生成的密文也是相同的。

因此,使用固定IV是不安全的,因为在攻击者拥有固定IV后,加密算法的弱点被发掘。最好的方法是使用随机 IV 来增加加密数据的安全性。
10个月前 评论
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
DogLoML (作者) 10个月前
wangtufly (楼主) 10个月前
wangtufly (楼主) 10个月前
DogLoML (作者) 10个月前
Rache1 10个月前
2daye 2周前
云客网络工作室

手机号真实数据可以使用加密,然后再增加一个 md5的手机号信息,检索可以md5之后检索,取出来的话就解密出来。md5加密可以加个盐嘛

10个月前 评论

目前有两只解决方法,一是重写一下内置的加密,变成固定向量的。二是加一列哈希,这两种方法在便利以及安全程度上考虑我觉得替换一下原方法更好一些

10个月前 评论

为啥不采用 SM4 加密,可加可解,你这样存起来手机号以后都不用啦?

10个月前 评论
wangtufly (楼主) 10个月前

加解密使用非对称加密就行

10个月前 评论
1.只考虑你目前的需求
不要把注意力放在加密手机号上面,你需要的是登录手机号验证时,快速查找到对应的手机号记录,新增字段做hash或者自定义的算法根据手机号生成唯一标识,算法结果长度应是固定且较短的(可以考虑用char类型存储),设置索引便于数据库查询,同时减少存储时的文件碎片,密文一般较长,作为加密的结果,如果是双向加密,势必业务上会有解密,不过也仅只是应用在业务加解密上,对于数据库层面来说,用密文查询校对,这么设置是不合理的,尤其是大数据表。
2.后续扩展
从前台到后台,用户是个重要的业务模块,都有可能需要展示用户的手机号(脱敏展示)或者用于营销业务(例如发短信等),方便运营操作,所以解密密文,所需要的key、iv、加密算法等等,理论上也要存储的,因为后续一旦更改key、iv、加密算法,解密就会有问题,iv是否随机这个对于你目前的业务来说,其实是无关紧要,它影响的只是一定的密文安全性。
10个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!