当前位置: 首页 > news >正文

(数据库学习四)哈希处理

首先我们需要了解什么是哈希处理,这个是我在处理使用数据库用户密码登录是做出处理。

简单来说,对数据库中的用户密码进行哈希处理,最根本的目的是为了保护用户密码,即使数据库泄露,攻击者也无法直接获取用户的明文密码

1. 核心原因:防范数据泄露的灾难性后果

数据库被黑客攻破(俗称“拖库”)是常见的安全事件。如果密码以明文形式存储,后果不堪设想:

  • 直接账户被盗:攻击者拿到了你的邮箱和密码,可以轻松登录你的账户,窃取敏感信息、进行恶意操作(如转账、发诈骗信息)。

  • 撞库攻击:绝大多数用户在不同网站使用相同或相似的密码。攻击者用从你这里泄露的邮箱和密码,去尝试登录其他重要网站(如支付宝、网上银行、社交网络),成功率极高,造成连锁反应。

2. 哈希(Hashing)是如何解决这个问题的?

哈希是一种单向的密码学函数。它把任意长度的输入(如密码),通过一个数学算法,转换成一个固定长度的、看似随机的字符串(即哈希值)。

关键特性:

  • 确定性:相同的输入永远产生相同的哈希值。

  • 单向性:从哈希值几乎不可能反向推导出原始输入。这是一个数学上的困难问题。

  • 雪崩效应:输入的微小改变(哪怕只改一个字符),会导致输出的哈希值发生巨大、不可预测的变化。

  • 抗碰撞性:极难找到两个不同的输入,但它们的哈希值相同。

工作流程对比:

不安全的方式(存储明文密码):

  1. 用户注册:输入密码 myPassword123

  2. 数据库直接存储:myPassword123

  3. 黑客窃取数据库后,直接看到了所有密码。

安全的方式(存储哈希值):

  1. 用户注册:输入密码 myPassword123

  2. 系统立即对该密码进行哈希运算,得到类似 $2a$10$X5hFD8S4f...WQZAFdd9e 的字符串。

  3. 数据库存储这个哈希字符串,并丢弃明文密码

  4. 用户登录时,再次对输入的密码进行相同的哈希运算。

  5. 系统比较此次运算的哈希值,与数据库中存储的哈希值是否一致。

  6. 黑客即使窃取了数据库,也只能看到一堆无意义的哈希字符串,无法直接使用它们来登录。


3. 仅仅哈希就够了吗?不,还需要“加盐”

单纯的哈希在当今已经不够安全。攻击者会使用一种叫做 “彩虹表” 的预先计算好的哈希字典来反向查询密码。为了对抗这种攻击,我们必须使用 “加盐”

什么是“盐”?

  • “盐”是一串随机生成的、足够长的字符。

  • 每个用户在注册时,系统都会为他生成一个独一无二的“盐”。

加盐哈希的工作流程:

  1. 用户注册:输入密码 myPassword123

  2. 系统为这个用户生成一个唯一的随机盐,例如 xQmF8pG2

  3. 系统将密码和盐组合在一起,然后进行哈希运算:哈希( myPassword123 + xQmF8pG2 )

  4. 将最终得到的哈希值  这个“盐”一起存入数据库的用户记录中。

加盐的巨大优势:

  • 破解彩虹表失效:彩虹表是针对常见密码的哈希值预先计算好的。由于每个用户都有自己独特的、随机的盐,攻击者必须为每个用户、每个盐重新计算彩虹表,这使得攻击的计算成本变得无法承受。

  • 即使密码相同,哈希值也不同:即使两个用户使用了相同的密码 123456,由于他们的盐不同,最终存储在数据库里的哈希值也完全不同。攻击者无法通过对比哈希值来发现哪些用户使用了弱密码。

4. 项目案例

当我写了登录接口,并在数据库配置了用户表后,尝试使用用户表数据登录。发现无法登录,页面无跳转,检查了没有存在跨域问题。

id=1,是我使用哈希处理后的密码,而往后的几条数据都是直接使用明文形式存储。而我在后端处理登录的时候使用bcrypt.CompareHashAndPassword函数来验证密码,这个函数期望数据库中的密码是经过哈希处理的。所以​当尝试验证未哈希的密码时,bcrypt.CompareHashAndPassword会返回错误,导致登录失败。

这时候就需要使用bcrypt算法进行哈希处理,可以删除原有的数据库数据,对新注册的用户密码进行处理,也可以对原有的数据进行批量处理。由于我第一次自己写后端没有注意细节,正常是直接第五步处理。

后端语言以go语言为例,提供一下思路。将现有数据库中的明文密码迁移到 bcrypt 哈希。详细步骤如下:

数据库明文密码迁移到 bcrypt 哈希

1. 数据库准备

首先修改用户表,添加 password_hash 字段:

-- 添加新字段用于存储 bcrypt 哈希
ALTER TABLE users ADD COLUMN password_hash VARCHAR(255);-- 确保原密码字段仍然保留(用于迁移期间)
-- password 字段应该已经存在,存储着明文密码

2. Go 数据模型

package modelsimport ("database/sql""time"
)type User struct {ID           int       `json:"id"`Email        string    `json:"email"`Password     string    `json:"-"` // 明文密码,不序列化到JSONPasswordHash string    `json:"-"` // bcrypt 哈希CreatedAt    time.Time `json:"created_at"`UpdatedAt    time.Time `json:"updated_at"`
}

3. 核心迁移函数

package servicesimport ("database/sql""fmt""log""golang.org/x/crypto/bcrypt"
)type UserService struct {DB *sql.DB
}// MigrateAllPasswords 一次性迁移所有用户密码(适用于维护期间)
func (s *UserService) MigrateAllPasswords() error {// 获取所有需要迁移的用户(password_hash 为空且 password 不为空)rows, err := s.DB.Query(`SELECT id, password FROM users WHERE password_hash IS NULL AND password IS NOT NULL AND password != ''`)if err != nil {return fmt.Errorf("查询用户失败: %v", err)}defer rows.Close()var migrated, failed intfor rows.Next() {var userID intvar plainPassword stringif err := rows.Scan(&userID, &plainPassword); err != nil {log.Printf("读取用户数据失败 (ID: %d): %v", userID, err)failed++continue}// 生成 bcrypt 哈希hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)if err != nil {log.Printf("密码哈希失败 (用户ID: %d): %v", userID, err)failed++continue}// 更新数据库_, err = s.DB.Exec(`UPDATE users SET password_hash = $1, password = '' WHERE id = $2`, string(hashedPassword), userID)if err != nil {log.Printf("更新数据库失败 (用户ID: %d): %v", userID, err)failed++continue}migrated++if migrated%100 == 0 {log.Printf("已迁移 %d 个用户密码", migrated)}}log.Printf("密码迁移完成: 成功=%d, 失败=%d", migrated, failed)return nil
}

4. 登录时的渐进式迁移

package authimport ("database/sql""errors""log""golang.org/x/crypto/bcrypt"
)type AuthService struct {DB *sql.DB
}// VerifyLogin 验证登录,并在成功时自动迁移密码
func (s *AuthService) VerifyLogin(email, password string) (*models.User, error) {// 查询用户信息var user models.Uservar storedPlainPassword sql.NullStringvar storedPasswordHash sql.NullStringerr := s.DB.QueryRow(`SELECT id, email, password, password_hash, created_at, updated_atFROM users WHERE email = $1`, email).Scan(&user.ID, &user.Email, &storedPlainPassword, &storedPasswordHash,&user.CreatedAt, &user.UpdatedAt,)if err != nil {if err == sql.ErrNoRows {return nil, errors.New("用户不存在")}return nil, fmt.Errorf("数据库查询失败: %v", err)}// 情况1:用户已迁移 (使用 password_hash)if storedPasswordHash.Valid && storedPasswordHash.String != "" {err := bcrypt.CompareHashAndPassword([]byte(storedPasswordHash.String), []byte(password),)if err != nil {return nil, errors.New("密码错误")}return &user, nil}// 情况2:用户尚未迁移 (使用明文密码)if storedPlainPassword.Valid && storedPlainPassword.String != "" {// 比较明文密码if password != storedPlainPassword.String {return nil, errors.New("密码错误")}// 密码正确,立即进行迁移hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)if err != nil {log.Printf("密码哈希生成失败 (用户ID: %d): %v", user.ID, err)// 即使哈希失败,也允许登录(但不会迁移)return &user, nil}// 更新数据库_, err = s.DB.Exec(`UPDATE users SET password_hash = $1, password = '' WHERE id = $2`, string(hashedPassword), user.ID)if err != nil {log.Printf("密码迁移更新失败 (用户ID: %d): %v", user.ID, err)// 即使更新失败,也允许登录} else {log.Printf("用户密码已迁移 (用户ID: %d)", user.ID)}return &user, nil}return nil, errors.New("密码未设置")
}

5. 新用户注册和密码修改

// RegisterUser 新用户注册 - 直接使用 bcrypt
func (s *AuthService) RegisterUser(email, password string) (*models.User, error) {// 生成密码哈希hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)if err != nil {return nil, fmt.Errorf("密码加密失败: %v", err)}var user models.Usererr = s.DB.QueryRow(`INSERT INTO users (email, password_hash, created_at, updated_at)VALUES ($1, $2, NOW(), NOW())RETURNING id, email, created_at, updated_at`, email, string(hashedPassword)).Scan(&user.ID, &user.Email, &user.CreatedAt, &user.UpdatedAt,)if err != nil {return nil, fmt.Errorf("创建用户失败: %v", err)}return &user, nil
}// ChangePassword 修改密码 - 直接使用 bcrypt
func (s *AuthService) ChangePassword(userID int, newPassword string) error {hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)if err != nil {return fmt.Errorf("密码加密失败: %v", err)}_, err = s.DB.Exec(`UPDATE users SET password_hash = $1, password = '', updated_at = NOW()WHERE id = $2`, string(hashedPassword), userID)if err != nil {return fmt.Errorf("更新密码失败: %v", err)}return nil
}

6. 监控和清理

// GetMigrationStats 获取迁移统计信息
func (s *UserService) GetMigrationStats() (map[string]int, error) {stats := make(map[string]int)// 总用户数err := s.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&stats["total_users"])if err != nil {return nil, err}// 已迁移用户数err = s.DB.QueryRow(`SELECT COUNT(*) FROM users WHERE password_hash IS NOT NULL AND password_hash != ''`).Scan(&stats["migrated_users"])if err != nil {return nil, err}// 待迁移用户数err = s.DB.QueryRow(`SELECT COUNT(*) FROM users WHERE password_hash IS NULL AND password IS NOT NULL AND password != ''`).Scan(&stats["pending_migration"])if err != nil {return nil, err}stats["migration_progress"] = (stats["migrated_users"] * 100) / stats["total_users"]return stats, nil
}// CleanupPlainTextPasswords 清理所有明文密码(迁移完成后)
func (s *UserService) CleanupPlainTextPasswords() error {_, err := s.DB.Exec("UPDATE users SET password = ''")if err != nil {return fmt.Errorf("清理明文密码失败: %v", err)}log.Println("所有明文密码已清理")return nil
}

总结:为什么必须做哈希处理?

  1. 责任与信任:保护用户密码是开发者的基本责任。用户信任你,你就不应该以任何可读的形式存储他们最敏感的秘密。

  2. 纵深防御:这是安全领域的基本原则。假设防线一定会被突破(数据库泄露),那么就要在防线后设置另一道防线(哈希过的密码无法使用)。

  3. 防止连锁反应:避免因一个网站的安全漏洞,导致用户在其他网站上的账户也陷入危险。

  4. 合规要求:许多数据安全法规和标准(如GDPR, PCI DSS)都明确要求对用户密码进行安全加密(通常指加盐哈希)。

最佳实践总结:

  • 永远不要以明文形式存储密码。

  • 始终使用强加密哈希算法,如 bcrypt, Argon2, PBKDF2。这些算法设计得速度很慢,可以有效对抗暴力破解。避免使用 已被证明不安全的快速哈希算法,如 MD5, SHA1。

  • 必须为每个密码加盐,且盐必须是随机、唯一的。

  • 在密码哈希处理完成后,立即从内存中清除明文密码。

通过这一系列措施,可以确保即使在最坏的情况下(数据库完全泄露),用户的密码本身仍然是安全的,从而将损失降到最低。

http://www.dtcms.com/a/464880.html

相关文章:

  • Hibernate中StatelessSession和‌普通Session对比
  • 浙江邮电工程建设有限公司网站企业门户网站建站
  • K8S1.28.2安装与部署
  • 业务知识:强制平仓
  • 币安最高多少杠杆 margin
  • wpf之GroupBox
  • 标签_图片(本文为个人学习笔记,内容整理自哔哩哔哩UP主【非学者勿扰】的公开课程。 > 所有知识点归属原作者,仅作非商业用途分享)
  • Jira:理解jira / 核心功能 / 应用场景 / 优势特点 / 常见的Jira术语
  • 专门找建筑案例的网站深圳展览展示公司排行
  • ACM算法梳理:
  • K8s集群多节点部署(Ubuntu22.04)
  • OpenCV(七):BGR
  • 仍可绕过:新变通方案可实现微软 Win11 装机 OOBE 创建本地账号
  • 深圳网站建设联系电话seo策略是什么
  • VS2026+QT6.9+opencv图像增强(多帧平均降噪)(CLAHE对比度增强)(边缘增强)(图像超分辨率)
  • Java 开发面试题(多线程模块)
  • 17-基于STM32的宠物饲养系统设计与实现
  • Docker镜像构建指南:Dockerfile语法与docker build命令全解析
  • 网页模板网站推荐网站每天更新多少文章
  • 三大数学工具在深度学习中的本质探讨:从空间表示到动态优化
  • 力扣1234. 替换子串得到平衡字符串
  • 数据链路层协议之STP协议
  • 给Windows电脑重命名有啥好处?
  • 网站后期的维护管理淘宝无货源一键铺货软件
  • 网站开发工程师是干嘛的网站开发职位
  • Java 创建 Word 文档:实现高效文档生成
  • C#限制当前单元格的值为指定值时禁止编辑的方法
  • 【gdb/sqlite3移植/mqtt】
  • 2025年渗透测试面试题总结-106(题目+回答)
  • 使用verdaccio搭建轻量的npm私有仓库