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:
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest"
|
||||
import { Shrimp, plainHasher, bcryptHasher, UserModule, MemoryShrimpCrud, ShrimpFieldTypes } from "../src"
|
||||
import type { IPasswordHasher } from "../src"
|
||||
|
||||
describe("PasswordHasher 抽象", () => {
|
||||
const original = Shrimp.passwordHasher
|
||||
afterEach(() => {
|
||||
Shrimp.passwordHasher = original
|
||||
})
|
||||
|
||||
it("默认是 bcryptHasher", () => {
|
||||
expect(Shrimp.passwordHasher).toBe(bcryptHasher)
|
||||
})
|
||||
|
||||
it("plainHasher: hash 原样返回,verify 字符串等值", async () => {
|
||||
expect(await plainHasher.hash("secret")).toBe("secret")
|
||||
expect(await plainHasher.verify("secret", "secret")).toBe(true)
|
||||
expect(await plainHasher.verify("secret", "other")).toBe(false)
|
||||
})
|
||||
|
||||
it("通过替换 Shrimp.passwordHasher 让 Password 字段走自定义哈希", async () => {
|
||||
const calls: string[] = []
|
||||
const custom: IPasswordHasher = {
|
||||
async hash(plain) { calls.push("hash:" + plain); return "X:" + plain },
|
||||
async verify(plain, hashed) { calls.push("verify:" + plain + ":" + hashed); return hashed === "X:" + plain },
|
||||
}
|
||||
Shrimp.passwordHasher = custom
|
||||
|
||||
const shrimp = new Shrimp()
|
||||
const userCrud = new MemoryShrimpCrud<{ _id: string; username: string; password: string; active: boolean; roleId?: string[] }>()
|
||||
const userModule = new UserModule(shrimp, userCrud as any, [
|
||||
{ key: "username", label: "用户名", type: ShrimpFieldTypes.String, required: true, editable: "addOnly" },
|
||||
{ key: "password", label: "密码", type: ShrimpFieldTypes.Password, required: true, tableHide: true },
|
||||
{ key: "active", label: "启用", type: ShrimpFieldTypes.Boolean, defaultValue: true },
|
||||
])
|
||||
|
||||
await userModule.add({ username: "alice", password: "pw123", active: true, roleId: [] } as any)
|
||||
const stored = (await userCrud.list({})).list[0]
|
||||
expect(stored.password).toBe("X:pw123")
|
||||
expect(calls).toContain("hash:pw123")
|
||||
|
||||
const ok = await userModule.getUser("alice", "pw123")
|
||||
expect(ok.username).toBe("alice")
|
||||
expect(calls.some(c => c.startsWith("verify:pw123:X:pw123"))).toBe(true)
|
||||
})
|
||||
|
||||
it("plainHasher 端到端: 在不装 bcrypt 的场景下完成登录校验", async () => {
|
||||
Shrimp.passwordHasher = plainHasher
|
||||
|
||||
const shrimp = new Shrimp()
|
||||
const userCrud = new MemoryShrimpCrud<{ _id: string; username: string; password: string; active: boolean; roleId?: string[] }>()
|
||||
const userModule = new UserModule(shrimp, userCrud as any, [
|
||||
{ key: "username", label: "用户名", type: ShrimpFieldTypes.String, required: true, editable: "addOnly" },
|
||||
{ key: "password", label: "密码", type: ShrimpFieldTypes.Password, required: true, tableHide: true },
|
||||
{ key: "active", label: "启用", type: ShrimpFieldTypes.Boolean, defaultValue: true },
|
||||
])
|
||||
|
||||
await userModule.add({ username: "bob", password: "open", active: true, roleId: [] } as any)
|
||||
const user = await userModule.getUser("bob", "open")
|
||||
expect(user.username).toBe("bob")
|
||||
|
||||
await expect(userModule.getUser("bob", "wrong")).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user