如何使用PDO安全实现SQL Server加密密码的显示、隐藏与更新
你给出的
password_hash+password_verify是单向哈希方案,不可逆无法解密出原始明文,仅适用于用户登录密码校验场景。你需要支持查看明文密码,必须改用对称加密方案,以下是完整实现方案:
一、基础架构设计
1. 加密方案选择
使用AES-256-GCM对称加密算法,自带完整性校验防篡改,完全满足加解密需求。
2. 存储规则
- 加密密钥:存储在服务端非Web可访问目录的配置文件(如.env)中,禁止硬编码到代码或存入数据库,文件权限设置为仅PHP运行用户可读。
- 数据库存储:SQL Server中存储base64编码后的密文(包含IV、校验Tag、加密内容),字段类型可用
VARCHAR(255)。 - 权限表:存储用户权限,区分
view_pwd(查看密码)、edit_pwd(编辑密码)两种权限。 - 审计日志表:记录用户ID、操作类型、操作时间、操作的密码记录ID,日志需设置为不可删除、不可修改。
二、后端PHP+PDO实现
1. 加解密工具函数
// 从配置读取密钥,AES-256要求密钥长度为32位 define('AES_KEY', $_ENV['AES_256_KEY']); // 加密函数 function encryptPassword(string $plainText): string|false { $ivLength = openssl_cipher_iv_length('aes-256-gcm'); $iv = openssl_random_pseudo_bytes($ivLength); $tag = ''; $encrypted = openssl_encrypt( $plainText, 'aes-256-gcm', AES_KEY, OPENSSL_RAW_DATA, $iv, $tag ); // 拼接IV、校验Tag、密文后base64编码,方便存储 return base64_encode($iv . $tag . $encrypted); } // 解密函数 function decryptPassword(string $encryptedText): string|false { $decoded = base64_decode($encryptedText); $ivLength = openssl_cipher_iv_length('aes-256-gcm'); $iv = substr($decoded, 0, $ivLength); $tag = substr($decoded, $ivLength, 16); // GCM校验Tag固定为16位 $encrypted = substr($decoded, $ivLength + 16); return openssl_decrypt( $encrypted, 'aes-256-gcm', AES_KEY, OPENSSL_RAW_DATA, $iv, $tag ); }
2. PDO操作封装(连接SQL Server)
需提前安装pdo_sqlsrv扩展支持SQL Server连接:
// 初始化PDO连接 $pdo = new PDO("sqlsrv:Server=你的数据库地址;Database=你的库名", "数据库账号", "数据库密码"); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 查询解密密码接口 function getDecryptedPwd(PDO $pdo, int $recordId, int $currentUserId): string|false { // 先校验查看权限 $permCheck = $pdo->prepare("SELECT COUNT(1) FROM user_permissions WHERE user_id = ? AND permission_code = 'view_pwd'"); $permCheck->execute([$currentUserId]); if ($permCheck->fetchColumn() < 1) { return false; } // 查询加密密文 $stmt = $pdo->prepare("SELECT encrypted_pwd FROM pwd_storage WHERE id = ?"); $stmt->execute([$recordId]); $encrypted = $stmt->fetchColumn(); if (!$encrypted) return false; // 写入审计日志 $logStmt = $pdo->prepare("INSERT INTO audit_log (user_id, operation, target_id, operate_time) VALUES (?, 'view_password', ?, GETDATE())"); $logStmt->execute([$currentUserId, $recordId]); // 解密返回 return decryptPassword($encrypted); } // 更新密码接口 function updatePwd(PDO $pdo, int $recordId, int $currentUserId, string $newPlainPwd): bool { // 校验编辑权限 $permCheck = $pdo->prepare("SELECT COUNT(1) FROM user_permissions WHERE user_id = ? AND permission_code = 'edit_pwd'"); $permCheck->execute([$currentUserId]); if ($permCheck->fetchColumn() < 1) { return false; } // 加密新密码 $encrypted = encryptPassword($newPlainPwd); if (!$encrypted) return false; // 更新数据库 $stmt = $pdo->prepare("UPDATE pwd_storage SET encrypted_pwd = ? WHERE id = ?"); $res = $stmt->execute([$encrypted, $recordId]); // 写入审计日志 if ($res) { $logStmt = $pdo->prepare("INSERT INTO audit_log (user_id, operation, target_id, operate_time) VALUES (?, 'edit_password', ?, GETDATE())"); $logStmt->execute([$currentUserId, $recordId]); } return $res; }
三、前端交互实现
禁止初始渲染明文密码,必须通过异步请求触发权限校验后再返回明文,避免页面缓存泄露敏感信息:
<!DOCTYPE html> <html> <head> <title>密码管理</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> </head> <body> <input type="text" id="pwdInput" value="***********" readonly> <button id="showBtn">查看密码</button> <button id="editBtn" style="display: none;margin-left: 8px;">编辑密码</button> <script> // 页面初始化时根据用户权限控制编辑按钮显示 const hasEditPerm = <?php echo $currentUserHasEditPerm ? 'true' : 'false' ?>; if (hasEditPerm) { document.getElementById('editBtn').style.display = 'inline-block'; } // 查看密码逻辑 document.getElementById('showBtn').addEventListener('click', async () => { try { const res = await axios.post('/api/get-password.php', { record_id: 1, // 要查询的密码记录ID _csrf: '你的CSRF校验Token' }); if (res.data.success) { document.getElementById('pwdInput').value = res.data.password; } else { alert('无权限查看密码'); } } catch (e) { alert('请求失败,请重试'); } }); // 编辑密码逻辑可自行扩展:点击编辑按钮弹出输入框,提交新密码调用更新接口即可 </script> </body> </html>
四、安全加固建议
- 所有敏感操作接口必须校验登录态、权限、CSRF Token,防止越权和跨站请求伪造。
- 查看密码操作建议增加二次身份验证(如输入当前用户的登录密码、短信验证码)。
- 全链路使用HTTPS传输,避免明文密码被抓包窃取。
- 禁止前端日志、本地缓存存储明文密码,查看后的密码可设置30秒后自动隐藏。
内容的提问来源于stack exchange,提问作者Wimpie Norman
相关产品推荐
相关产品推荐

