AdonisJS v7 已完成功能开发,正在进行最终验证。 了解更多

哈希

哈希 (Hashing)

你可以使用 hash 服务在你的应用中对用户密码进行哈希处理。AdonisJS 对 bcryptscryptargon2 哈希算法提供了一流的支持,并且能够 添加自定义驱动程序

哈希值以 PHC 字符串格式 存储。PHC 是一种用于格式化哈希的确定性编码规范。

用法

hash.make 方法接受一个纯字符串值(用户密码输入)并返回哈希输出。

import hash from '@adonisjs/core/services/hash'
const hash = await hash.make('user_password')
// $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ

不能将哈希值转换为纯文本,哈希是一个单向过程,生成哈希后无法检索原始值。

但是,哈希提供了一种验证给定纯文本值是否与现有哈希匹配的方法,你可以使用 hash.verify 方法执行此检查。

import hash from '@adonisjs/core/services/hash'
if (await hash.verify(existingHash, plainTextValue)) {
// 密码正确
}

配置

哈希的配置存储在 config/hash.ts 文件中。默认驱动程序设置为 scrypt,因为 scrypt 使用 Node.js 原生 crypto 模块,不需要任何第三方包。

config/hash.ts
import { defineConfig, drivers } from '@adonisjs/core/hash'
export default defineConfig({
default: 'scrypt',
list: {
scrypt: drivers.scrypt(),
/**
* 使用 argon2 时取消注释
argon: drivers.argon2(),
*/
/**
* 使用 bcrypt 时取消注释
bcrypt: drivers.bcrypt(),
*/
}
})

Argon

Argon 是推荐用于哈希用户密码的哈希算法。要在 AdonisJS 哈希服务中使用 argon,必须安装 argon2 npm 包。

npm i argon2

我们使用安全默认值配置 argon 驱动程序,但请随意根据你的应用需求调整配置选项。以下是可用选项的列表。

export default defineConfig({
// 确保将默认驱动程序更新为 argon
default: 'argon',
list: {
argon: drivers.argon2({
version: 0x13, // hex code for 19
variant: 'id',
iterations: 3,
memory: 65536,
parallelism: 4,
saltSize: 16,
hashLength: 32,
})
}
})

variant

要使用的 argon 哈希变体。

  • d 更快且对 GPU 攻击具有高度抵抗力,这对于加密货币很有用
  • i 较慢且对权衡攻击具有抵抗力,这对于密码哈希和密钥派生是首选。
  • id (默认) 是上述两者的混合组合,可抵抗 GPU 和权衡攻击。

version

要使用的 argon 版本。可用选项为 0x10 (1.0)0x13 (1.3)。默认情况下应使用最新版本。

iterations

iterations 计数增加哈希强度,但也需要更多时间来计算。

默认值为 3

memory

用于哈希值的内存量。每个并行线程将拥有此大小的内存池。

默认值为 65536 (KiB)

parallelism

用于计算哈希的线程数。

默认值为 4

saltSize

盐的长度(以字节为单位)。Argon 在计算哈希时会生成此大小的加密安全随机盐。

密码哈希的默认和推荐值为 16

hashLength

原始哈希的最大长度(以字节为单位)。输出值将长于提到的哈希长度,因为原始哈希输出进一步编码为 PHC 格式。

默认值为 32

Bcrypt

要在 AdonisJS 哈希服务中使用 Bcrypt,必须安装 bcrypt npm 包。

npm i bcrypt

以下是可用配置选项的列表。

export default defineConfig({
// 确保将默认驱动程序更新为 bcrypt
default: 'bcrypt',
list: {
bcrypt: drivers.bcrypt({
rounds: 10,
saltSize: 16,
version: 98
})
}
})

rounds

计算哈希的成本。我们建议阅读 Bcrypt 文档中的 关于 Rounds 的说明 部分,了解 rounds 值如何影响计算哈希所需的时间。

默认值为 10

saltSize

盐的长度(以字节为单位)。计算哈希时,我们会生成此大小的加密安全随机盐。

默认值为 16

version

哈希算法的版本。支持的值为 9798。建议使用最新版本,即 98

Scrypt

scrypt 驱动程序使用 Node.js crypto 模块计算密码哈希。配置选项与 Node.js scrypt 方法 接受的选项相同。

export default defineConfig({
// 确保将默认驱动程序更新为 scrypt
default: 'scrypt',
list: {
scrypt: drivers.scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
saltSize: 16,
maxMemory: 33554432,
keyLength: 64
})
}
})

使用模型钩子哈希密码

由于你将使用 hash 服务来哈希用户密码,你可能会发现将逻辑放在 beforeSave 模型钩子中很有帮助。

如果你使用的是 @adonisjs/auth 模块,则无需在模型中哈希密码。AuthFinder 会自动处理密码哈希,确保你的用户凭据得到安全处理。在 此处 了解有关此过程的更多信息。

import { BaseModel, beforeSave } from '@adonisjs/lucid'
import hash from '@adonisjs/core/services/hash'
export default class User extends BaseModel {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}

在驱动程序之间切换

如果你的应用使用多个哈希驱动程序,你可以使用 hash.use 方法在它们之间切换。

hash.use 方法接受配置文件中的映射名称,并返回匹配驱动程序的实例。

import hash from '@adonisjs/core/services/hash'
// 使用配置文件中的 "list.scrypt" 映射
await hash.use('scrypt').make('secret')
// 使用配置文件中的 "list.bcrypt" 映射
await hash.use('bcrypt').make('secret')
// 使用配置文件中的 "list.argon" 映射
await hash.use('argon').make('secret')

检查密码是否需要重新哈希

建议使用最新的配置选项来保持密码安全,尤其是当旧版本的哈希算法报告漏洞时。

使用最新选项更新配置后,你可以使用 hash.needsReHash 方法检查密码哈希是否使用旧选项并执行重新哈希。

检查必须在用户登录期间执行,因为这是你唯一可以访问纯文本密码的时间。

import hash from '@adonisjs/core/services/hash'
if (await hash.needsReHash(user.password)) {
user.password = await hash.make(plainTextPassword)
await user.save()
}

如果你使用模型钩子计算哈希,你可以将纯文本值分配给 user.password

if (await hash.needsReHash(user.password)) {
// 让模型钩子重新哈希密码
user.password = plainTextPassword
await user.save()
}

在测试期间伪造哈希服务

哈希值通常是一个缓慢的过程,它会使你的测试变慢。因此,你可能会考虑使用 hash.fake 方法伪造哈希服务以禁用密码哈希。

在下面的示例中,我们使用 UserFactory 创建 20 个用户。由于你使用模型钩子来哈希密码,这可能需要 5-7 秒(取决于配置)。

import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
await UserFactory().createMany(20)
const response = await client.get('users')
})

但是,一旦你伪造了哈希服务,相同的测试将以快几个数量级的速度运行。

import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
hash.fake()
await UserFactory().createMany(20)
const response = await client.get('users')
hash.restore()
})

创建自定义哈希驱动程序

哈希驱动程序必须实现 HashDriverContract 接口。此外,官方哈希驱动程序使用 PHC 格式 序列化哈希输出以进行存储。你可以检查现有驱动程序的实现,看看它们如何使用 PHC 格式化程序 来生成和验证哈希。

import {
HashDriverContract,
ManagerDriverFactory
} from '@adonisjs/core/types/hash'
/**
* 哈希驱动程序接受的配置
*/
export type PbkdfConfig = {
}
/**
* 驱动程序实现
*/
export class Pbkdf2Driver implements HashDriverContract {
constructor(public config: PbkdfConfig) {
}
/**
* 检查哈希值是否按照哈希算法格式化。
*/
isValidHash(value: string): boolean {
}
/**
* 将原始值转换为哈希
*/
async make(value: string): Promise<string> {
}
/**
* 验证纯文本值是否与提供的哈希匹配
*/
async verify(
hashedValue: string,
plainValue: string
): Promise<boolean> {
}
/**
* 检查是否需要重新哈希,因为配置参数已更改
*/
needsReHash(value: string): boolean {
}
}
/**
* 在配置文件中引用驱动程序的工厂函数。
*/
export function pbkdf2Driver (config: PbkdfConfig): ManagerDriverFactory {
return () => {
return new Pbkdf2Driver(config)
}
}

在上面的代码示例中,我们导出了以下值。

  • PbkdfConfig: 你想要接受的配置的 TypeScript 类型。

  • Pbkdf2Driver: 驱动程序的实现。它必须遵守 HashDriverContract 接口。

  • pbkdf2Driver: 最后,一个用于延迟创建驱动程序实例的工厂函数。

使用驱动程序

一旦实现完成,你可以使用 pbkdf2Driver 工厂函数在配置文件中引用驱动程序。

config/hash.ts
import { defineConfig } from '@adonisjs/core/hash'
import { pbkdf2Driver } from 'my-custom-package'
export default defineConfig({
list: {
pbkdf2: pbkdf2Driver({
// 配置在这里
}),
}
})