feat: passwordHasher 可注入 — bcrypt 不再是 Shrimp 核心硬依赖

新增 IPasswordHasher 抽象 + bcryptHasher(默认,lazy import)+ plainHasher
(测试用)。Password 字段 reqFilter 和 UserModule.verifyPassword 都走
`Shrimp.passwordHasher`,消费方在启动期换实现就能彻底跳过 bcrypt。

  import { Shrimp, plainHasher } from "shrimp-server"
  Shrimp.passwordHasher = plainHasher

测试覆盖默认值、plain 端到端登录、自定义 hasher 被调用。
This commit is contained in:
2026-05-23 16:57:27 +08:00
parent 36326ac26b
commit 36b71bd38e
5 changed files with 150 additions and 11 deletions
+51
View File
@@ -0,0 +1,51 @@
/**
* 密码哈希抽象 — 把 bcrypt 从 Shrimp 核心彻底剥离
*
* 默认仍用 bcrypt(行业标准,Shrimp 大多数场景都需要),但通过抽象接口
* 让消费方可以:
* - 完全不装 bcrypt(走 plainHasher / 自己写一个 argon2 / scrypt 等)
* - 在测试里跳过哈希计算开销
* - 接入已有的密码系统(SHA256 + salt 之类)
*
* 配置方式:
* import { Shrimp, plainHasher } from "shrimp-server"
* Shrimp.passwordHasher = plainHasher // 或自己实现的 IPasswordHasher
*/
export interface IPasswordHasher {
/** 把明文密码加密成存储字符串 */
hash(plain: string): Promise<string>
/** 校验明文密码是否匹配已存储的哈希 */
verify(plain: string, hashed: string): Promise<boolean>
}
/**
* 默认实现 — bcrypt
*
* 通过动态 import 加载,确保只有真正调用 hash/verify 时才 require('bcrypt')。
* 这样消费方:不创建 Password 字段 + 不登录的话,可以完全不装 bcrypt。
*/
export const bcryptHasher: IPasswordHasher = {
async hash(plain: string): Promise<string> {
const bcrypt = await import("bcrypt")
return bcrypt.hash(plain, 10)
},
async verify(plain: string, hashed: string): Promise<boolean> {
const bcrypt = await import("bcrypt")
return bcrypt.compare(plain, hashed)
},
}
/**
* 明文实现 — 仅用于测试 / 开发环境 / 自己已经在上层做了加密
*
* **生产环境严禁使用**。存储明文密码的后果你懂的。
*/
export const plainHasher: IPasswordHasher = {
async hash(plain: string): Promise<string> {
return plain
},
async verify(plain: string, hashed: string): Promise<boolean> {
return plain === hashed
},
}
+10 -11
View File
@@ -6,18 +6,19 @@
import { ShrimpCode, IShrimpResponse, ITableCrudProto, IModuleInfo, IUser, IRole, WithId, IRoleProto, IPageQuery, IPageResult, IFieldConfig, IFieldRule, IFieldTypeHandler, IShrimpQuery, IShrimpCondition, IShrimpPopulate, ShrimpConfig, IShrimpOperator, IMenuItem, ShrimpError, ShrimpFieldTypes, ILabel } from "./IShrimpType"
import { AsyncLocalStorage } from 'async_hooks'
// 懒加载 bcrypt: 只有走到 Password 字段 hash 或 verifyPassword 才 import,
// 让纯核心(MemoryShrimpCrud + 业务模块)的消费方可以不装 bcrypt
async function loadBcrypt(): Promise<typeof import("bcrypt")> {
return await import("bcrypt")
}
import { IPasswordHasher, bcryptHasher } from './PasswordHasher'
export class Shrimp {
root: Shrimp = this
modules: Map<string, ShrimpModule> = new Map()
/** 全局 CRUD 注册表,用于 MongoShrimpCrud 自动解析 relation */
crudRegistry: Map<string, ShrimpCrud<any>> = new Map()
/**
* 密码哈希器,默认 bcryptHasher(lazy import bcrypt)。
* 消费方可在启动时替换:`Shrimp.passwordHasher = plainHasher` 或自定义实现,
* 让 Password 字段 reqFilter / UserModule.verifyPassword 都走自定义逻辑。
*/
static passwordHasher: IPasswordHasher = bcryptHasher
/** 请求级操作者上下文,通过 AsyncLocalStorage 避免并发覆盖 */
static asyncLocalStorage = new AsyncLocalStorage<IShrimpOperator | null>()
get operator(): IShrimpOperator | null {
@@ -258,8 +259,7 @@ export const defaultTypeHandlers: Record<string, IFieldTypeHandler> = {
[ShrimpFieldTypes.Password]: {
reqFilter: async (value) => {
if (typeof value === 'string' && value.length > 0) {
const bcrypt = await loadBcrypt()
return bcrypt.hash(value, 10)
return Shrimp.passwordHasher.hash(value)
}
return value
},
@@ -713,10 +713,9 @@ export class UserModule extends ShrimpTableModule<WithId<IUser>> {
this.initCrud()
}
/** 校验密码,子类可覆盖 */
/** 校验密码,子类可覆盖。默认走 `Shrimp.passwordHasher` 上挂载的哈希器。 */
protected async verifyPassword(storedPassword: string, plainPassword: string): Promise<boolean> {
const bcrypt = await loadBcrypt()
return bcrypt.compare(plainPassword, storedPassword)
return Shrimp.passwordHasher.verify(plainPassword, storedPassword)
}
/** 从用户对象提取角色 ID 列表,子类可覆盖 */
+4
View File
@@ -71,6 +71,10 @@ export type {
ShrimpCrud,
} from "./Shrimp"
// ===== 密码哈希(可注入,默认 bcrypt lazy import) =====
export { bcryptHasher, plainHasher } from "./PasswordHasher"
export type { IPasswordHasher } from "./PasswordHasher"
// ===== 数据源(纯内存,无外部依赖) =====
export { MemoryShrimpCrud } from "./MemoryShrimpCrud"