ThinkPHP项目密码强度与加密

wen PHP项目 3

本文目录导读:

ThinkPHP项目密码强度与加密

  1. 密码强度校验
  2. 密码加密存储
  3. 完整实战示例(注册+登录)
  4. 前端密码提示(用户体验)
  5. 常见安全建议总结
  6. 常见坑点规避

在ThinkPHP项目中,密码安全是核心安全项之一,下面从密码强度校验密码加密存储两个维度,结合ThinkPHP(5.x/6.x/8.x)的常用实践进行详细说明。


密码强度校验

通常在控制器(Controller)表单验证(Validate)层进行,ThinkPHP 5.1+及6/8版本均支持验证器。

使用验证器(推荐)

在应用目录下创建验证器类,app\common\validate\User.php

<?php
declare(strict_types=1);
namespace app\common\validate;
use think\Validate;
class User extends Validate
{
    protected $rule = [
        // ... 其他规则
        'password' => 'require|min:8|max:32|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
        'repassword' => 'require|confirm:password',
    ];
    protected $message = [
        'password.require' => '密码不能为空',
        'password.min'     => '密码至少8位',
        'password.max'     => '密码最长32位',
        'password.regex'   => '密码必须包含大写字母、小写字母和数字',
        'repassword.confirm' => '两次密码不一致',
    ];
}

正则解析 ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$

  • (?=.*[a-z]):包含小写字母。
  • (?=.*[A-Z]):包含大写字母。
  • (?=.*\d):包含数字。
  • 至少1个字符(与min:8配合则至少8位)。

控制器中调用

public function register()
{
    $data = $this->request->post();
    $validate = new \app\common\validate\User();
    if (!$validate->check($data)) {
        return json(['code' => 0, 'msg' => $validate->getError()]);
    }
    // 通过校验,继续注册逻辑
}

自定义强度规则(可选)

若需区分强度级别(弱/中/强),可编写一个辅助函数:

// 公共函数文件(如 common.php)
function password_strength(string $password, array &$score = null): string
{
    $score = 0;
    $len = strlen($password);
    if ($len >= 8) $score += 10;
    if ($len >= 12) $score += 10;
    if (preg_match('/[a-z]/', $password)) $score += 10;
    if (preg_match('/[A-Z]/', $password)) $score += 20;
    if (preg_match('/[0-9]/', $password)) $score += 20;
    if (preg_match('/[^a-zA-Z0-9]/', $password)) $score += 30;
    if ($score <= 30) return 'weak';
    if ($score <= 60) return 'medium';
    return 'strong';
}

密码加密存储

绝不使用MD5、SHA1、Base64明文存储。 ThinkPHP框架内置了安全的哈希算法支持。

使用 password_hash() / password_verify()(推荐)

这是PHP原生提供的密码哈希API,使用bcrypt算法,自动生盐。

加密(注册/修改密码时)

use think\facade\Hash;
// ThinkPHP 5.1+ 中内置了 think\facade\Hash 门面,封装了 password_hash
$hash = Hash::make(input('post.password')); // 等价于 password_hash($pwd, PASSWORD_DEFAULT)
// 或直接使用PHP原生函数
$hash = password_hash(input('post.password'), PASSWORD_DEFAULT);

注意:ThinkPHP的Hash门面实际上是对think\helper\Hash的封装,内部使用password_hash

验证(登录时)

$user = UserModel::where('username', $username)->find();
if (!$user) {
    return '用户不存在';
}
// 方式1:TP Hash门面
if (!Hash::check($password, $user->password)) {
    return '密码错误';
}
// 方式2:PHP原生
if (!password_verify($password, $user->password)) {
    return '密码错误';
}

使用ThinkPHP内置加密(老式兼容)

不应再用于新项目(TP5.0使用md5(sha1($password))方式,强烈不建议)。

若需对旧系统数据兼容,可以使用ThinkPHP的think\helper\Hash

use think\helper\Hash;
$hash = Hash::make('123456'); // 生成
$check = Hash::check('123456', $hash); // 验证 true

Hash门面内部实现(tp5.1/tp6/tp8):

// 实质
public static function make(string $value, array $options = []): string
{
    return password_hash($value, PASSWORD_BCRYPT, $options);
}
public static function check(string $value, string $hashedValue): bool
{
    return password_verify($value, $hashedValue);
}

加密成本配置(可选)

config/hash.php(TP6/8)中可调节加密成本,兼顾安全与性能:

<?php
// config/hash.php
return [
    // 加密算法
    'algo' => PASSWORD_BCRYPT,
    // 加密代价(bcrypt为4-31,默认10)
    'cost' => env('hash.cost', 10),
];

数据库字段设计

  • 字段名:password(或passwd
  • 类型:VARCHAR(255)(bcrypt哈希长度约60字符,必须留足)
  • 索引:不是搜索字段,无需加索引
CREATE TABLE `tp_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '密码哈希',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态',
  `create_time` int(11) DEFAULT NULL,
  `update_time` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

完整实战示例(注册+登录)

模型(User.php)

<?php
declare(strict_types=1);
namespace app\model;
use think\Model;
use think\facade\Hash;
class User extends Model
{
    // 自动时间戳
    protected $autoWriteTimestamp = true;
    // 密码修改器:写入前自动加密
    public function setPasswordAttr($value)
    {
        return Hash::make($value);
    }
    // 登录验证
    public static function login(string $username, string $password): bool
    {
        $user = self::where('username', $username)
                     ->where('status', 1)
                     ->find();
        if (!$user) {
            return false;
        }
        return Hash::check($password, $user->password);
    }
}

控制器(Auth.php)

<?php
declare(strict_types=1);
namespace app\controller;
use think\Request;
use app\model\User;
use app\common\validate\User as UserValidate;
class Auth
{
    // 注册
    public function register(Request $request)
    {
        $data = $request->post();
        $validate = new UserValidate();
        if (!$validate->scene('register')->check($data)) {
            return json(['code' => 0, 'msg' => $validate->getError()]);
        }
        $user = User::create([
            'username' => $data['username'],
            'password' => $data['password'], // 自动加密
        ]);
        return json(['code' => 1, 'msg' => '注册成功', 'data' => ['id' => $user->id]]);
    }
    // 登录
    public function login(Request $request)
    {
        $username = $request->post('username');
        $password = $request->post('password');
        // 验证用户
        $user = User::where('username', $username)->find();
        if (!$user || !password_verify($password, $user->password)) {
            return json(['code' => 0, 'msg' => '用户名或密码错误']);
        }
        // session 存储登录状态
        session('user_id', $user->id);
        session('username', $user->username);
        return json(['code' => 1, 'msg' => '登录成功']);
    }
    // 修改密码
    public function changePassword(Request $request)
    {
        $userId = session('user_id');
        if (!$userId) {
            return json(['code' => 0, 'msg' => '请先登录']);
        }
        $data = $request->post();
        // 验证旧密码
        $user = User::find($userId);
        if (!password_verify($data['old_password'], $user->password)) {
            return json(['code' => 0, 'msg' => '原密码错误']);
        }
        // 新密码强度验证
        if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,32}$/', $data['new_password'])) {
            return json(['code' => 0, 'msg' => '新密码强度不足']);
        }
        // 更新密码(使用修改器自动加密)
        $user->save(['password' => $data['new_password']]);
        return json(['code' => 1, 'msg' => '密码修改成功']);
    }
}

前端密码提示(用户体验)

建议在注册页面加入实时强度条:

<input type="password" id="password">
<div id="strength-bar">
    <span class="weak">弱</span>
    <span class="medium">中</span>
    <span class="strong">强</span>
</div>
<script>
document.getElementById('password').addEventListener('input', function () {
    const pwd = this.value;
    let score = 0;
    if (pwd.length >= 8) score += 10;
    if (pwd.length >= 12) score += 10;
    if (/[a-z]/.test(pwd)) score += 10;
    if (/[A-Z]/.test(pwd)) score += 20;
    if (/[0-9]/.test(pwd)) score += 20;
    if (/[^a-zA-Z0-9]/.test(pwd)) score += 30;
    let level = '弱';
    if (score > 60) level = '强';
    else if (score > 30) level = '中';
    // 更新UI
});
</script>

常见安全建议总结

原则 说明
编码前校验 Validate校验规则清晰,防止绕过
加密存储 password_hash() + password_verify()
最小长度 至少8位,推荐12位以上
组合要求 大小写+数字+特殊符号(可选)
防暴力破解 登录接口加验证码/频率限制
禁止泄露 错误信息不区分“用户不存在”还是“密码错误”
定期更新 提供“修改密码”功能,可设置过期策略

常见坑点规避

  1. 字段长度不够password字段写 VARCHAR(32) 会导致bcrypt哈希被截断 → 必须改为 VARCHAR(255)
  2. 验证规则冲突min:8regex同时使用可能冲突,注意正则要包含{8,32}量词。
  3. 修改密码时加密:使用模型修改器时,注意避免二次加密,如果字段已经加密,不要重复调用Hash::make()
  4. session安全:登录状态存储使用ThinkPHP的Session,并设置合理的过期时间。

如果有具体版本(如TP6或TP8)或特殊业务场景(如LDAP、OAuth),可以补充说明,我再针对性地展开。

抱歉,评论功能暂时关闭!