本文目录导读:

在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位以上 |
| 组合要求 | 大小写+数字+特殊符号(可选) |
| 防暴力破解 | 登录接口加验证码/频率限制 |
| 禁止泄露 | 错误信息不区分“用户不存在”还是“密码错误” |
| 定期更新 | 提供“修改密码”功能,可设置过期策略 |
常见坑点规避
- 字段长度不够:
password字段写VARCHAR(32)会导致bcrypt哈希被截断 → 必须改为VARCHAR(255)。 - 验证规则冲突:
min:8和regex同时使用可能冲突,注意正则要包含{8,32}量词。 - 修改密码时加密:使用模型修改器时,注意避免二次加密,如果字段已经加密,不要重复调用
Hash::make()。 - session安全:登录状态存储使用ThinkPHP的Session,并设置合理的过期时间。
如果有具体版本(如TP6或TP8)或特殊业务场景(如LDAP、OAuth),可以补充说明,我再针对性地展开。