哈希
您可以使用 hash 服务在应用程序中对用户密码进行哈希。AdonisJS 对 bcrypt、scrypt 和 argon2 哈希算法提供一流支持,并支持添加自定义驱动。
哈希值以 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 模块,不需要任何第三方包。
// title: 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({
// highlight-start
// 确保将默认驱动更新为 argon
default: 'argon',
// highlight-end
list: {
argon: drivers.argon2({
version: 0x13, // 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({
// highlight-start
// 确保将默认驱动更新为 bcrypt
default: 'bcrypt',
// highlight-end
list: {
bcrypt: drivers.bcrypt({
rounds: 10,
saltSize: 16,
version: 98
})
}
})rounds
计算哈希的成本。我们建议阅读 Bcrypt 文档中的关于轮次的说明部分,以了解
rounds值如何影响计算哈希所需的时间。默认值为
10。saltSize
盐的长度(以字节为单位)。计算哈希时,我们会生成此大小的加密安全随机盐。
默认值为
16。version
哈希算法的版本。支持的值为
97和98。建议使用最新版本,即98。
Scrypt
scrypt 驱动使用 Node.js crypto 模块计算密码哈希。配置选项与 Node.js scrypt 方法接受的选项相同。
export default defineConfig({
// highlight-start
// 确保将默认驱动更新为 scrypt
default: 'scrypt',
// highlight-end
list: {
scrypt: drivers.scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
saltSize: 16,
maxMemory: 33554432,
keyLength: 64
})
}
})使用模型钩子哈希密码
由于您将使用 hash 服务来哈希用户密码,您可能会发现将逻辑放在 beforeSave 模型钩子中很有帮助。
TIP
如果您使用 @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 }) => {
// highlight-start
hash.fake()
// highlight-end
await UserFactory().createMany(20)
const response = await client.get('users')
// highlight-start
hash.restore()
// highlight-end
})创建自定义哈希驱动
哈希驱动必须实现 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 工厂函数在配置文件中引用驱动。
// title: config/hash.ts
import { defineConfig } from '@adonisjs/core/hash'
import { pbkdf2Driver } from 'my-custom-package'
export default defineConfig({
list: {
pbkdf2: pbkdf2Driver({
// 配置在这里
}),
}
})