Skip to content

哈希

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

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

使用

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

ts
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 方法执行此检查。

ts
import hash from '@adonisjs/core/services/hash'

if (await hash.verify(existingHash, plainTextValue)) {
  // 密码正确
}

配置

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

ts
// 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 包。

sh
npm i argon2

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

ts
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 包。

sh
npm i bcrypt

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

ts
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

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

Scrypt

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

ts
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 会自动处理密码哈希,确保您的用户凭证得到安全处理。在此处了解更多关于此过程的信息。

ts
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 方法接受配置文件中的映射名称,并返回匹配驱动的实例。

ts
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 方法检查密码哈希是否使用旧选项并执行重新哈希。

检查必须在用户登录期间进行,因为那是您唯一可以访问明文密码的时间。

ts
import hash from '@adonisjs/core/services/hash'

if (await hash.needsReHash(user.password)) {
  user.password = await hash.make(plainTextPassword)
  await user.save()
}

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

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

在测试期间模拟哈希服务

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

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

ts
import hash from '@adonisjs/core/services/hash'

test('get users list', async ({ client }) => {
  await UserFactory().createMany(20)    
  const response = await client.get('users')
})

但是,一旦您模拟哈希服务,同样的测试将运行得快得多。

ts
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 格式化程序来创建和验证哈希。

ts
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 工厂函数在配置文件中引用驱动。

ts
// title: config/hash.ts
import { defineConfig } from '@adonisjs/core/hash'
import { pbkdf2Driver } from 'my-custom-package'

export default defineConfig({
  list: {
    pbkdf2: pbkdf2Driver({
      // 配置在这里
    }),
  }
})