跳转到内容

重置密码

让用户在验证账户所有权后重置密码。

本指南添加 reset-password 功能。

reset-password 功能让用户在证明自己拥有该账户后设置新密码。它把 otp 或 magic-link 这样的功能用作验证策略。这些功能不仅可以用于登录,也可以被其他需要验证账户所有权的流程复用。

添加功能

运行 CLI 命令:

Terminal window
brkpt auth add reset-password

CLI 会添加一个新的 features/reset-password/ 文件夹。

校验 DTO

reset password 功能使用固定的请求结构。本指南已经启用了 ValidationPipe,所以只需为生成的 DTO 添加校验装饰器。

src/brkpt-auth/features/reset-password/dto/send.dto.ts
import { IsString } from 'class-validator';
export class SendDto {
@IsString()
target!: string;
@IsString()
strategy!: string;
@IsString()
method!: string;
}
src/brkpt-auth/features/reset-password/dto/reset.dto.ts
import { IsOptional, IsString, MinLength } from 'class-validator';
export class ResetDto {
@IsOptional()
@IsString()
target?: string;
@IsString()
strategy!: string;
@IsString()
proof!: string;
@IsString()
@MinLength(6)
newPassword!: string;
}

strategy 选择用来证明账户所有权的验证策略。本指南同时启用了 otp 和 magic-link。

proof 包含该策略生成的验证凭证:otp 对应一个 OTP 验证码,magic-link 对应一个令牌。

在重置步骤中 target 是可选的,因为不同策略解析已验证 target 的方式不同。OTP 需要 target 来定位并校验验证码,而 magic link 可以直接从它的令牌中还原出 target。

实现适配器

创建一个适配器,根据已验证的 target 查找用户并更新其密码:

  • 文件夹src/
    • 文件夹brkpt-auth/
      • 文件夹adapters/
        • reset-password.adapter.ts
src/brkpt-auth/adapters/reset-password.adapter.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { User } from '../../../generated/prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { ResetPasswordPort } from '../features/reset-password/reset-password.port';
@Injectable()
export class ResetPasswordAdapter implements ResetPasswordPort<User> {
constructor(private readonly prisma: PrismaService) {}
async findUserByTarget(method: string, target: string): Promise<User | null> {
switch (method) {
case 'email':
return this.prisma.user.findUnique({
where: { email: target },
});
}
return null;
}
async updatePassword(user: User, newPassword: string): Promise<void> {
const password = await bcrypt.hash(newPassword, 10);
await this.prisma.user.update({
where: { id: user.id },
data: { password },
});
}
extractUserIdFromUser(user: User): number {
return user.id;
}
}

注册功能

更新 features.ts,把 ResetPasswordAdapter 传给 resetPasswordFeature:

src/brkpt-auth/features.ts
import { BlacklistAdapter } from './adapters/blacklist.adapter';
import { ChangePasswordAdapter } from './adapters/change-password.adapter';
import { CoreAdapter } from './adapters/core.adapter';
import { CredentialsAdapter } from './adapters/credentials.adapter';
import { MagicLinkAdapter } from './adapters/magic-link.adapter';
import { OAuthAdapter } from './adapters/oauth.adapter';
import { OtpAdapter } from './adapters/otp.adapter';
import { ResetPasswordAdapter } from './adapters/reset-password.adapter';
import { SessionAdapter } from './adapters/session.adapter';
import { FeatureConfig } from './common/interfaces';
import { blacklistFeature } from './features/blacklist/blacklist.feature';
import { changePasswordFeature } from './features/change-password/change-password.feature';
import { coreFeature } from './features/core/core.feature';
import { credentialsFeature } from './features/credentials/credentials.feature';
import { EmailMagicLinkDriver } from './features/magic-link/drivers/email.driver';
import { magicLinkFeature } from './features/magic-link/magic-link.feature';
import { GithubOAuthDriver } from './features/oauth/drivers/github.driver';
import { GoogleOAuthDriver } from './features/oauth/drivers/google.driver';
import { oauthFeature } from './features/oauth/oauth.feature';
import { EmailOtpDriver } from './features/otp/drivers/email.driver';
import { otpFeature } from './features/otp/otp.feature';
import { resetPasswordFeature } from './features/reset-password/reset-password.feature';
import { sessionFeature } from './features/session/session.feature';
export const features: FeatureConfig[] = [
coreFeature(CoreAdapter),
blacklistFeature(BlacklistAdapter),
credentialsFeature(CredentialsAdapter),
sessionFeature(SessionAdapter),
oauthFeature(OAuthAdapter, GoogleOAuthDriver, GithubOAuthDriver),
otpFeature(OtpAdapter, EmailOtpDriver),
magicLinkFeature(MagicLinkAdapter, EmailMagicLinkDriver),
changePasswordFeature(ChangePasswordAdapter),
resetPasswordFeature(ResetPasswordAdapter),
];

结合前面几篇指南的配置,OTP 可以直接使用。要让 magic link 也能用于重置密码,需要添加一个 resetPassword 回调 URL:

src/app.module.ts
magicLink: {
expiresIn: '5m',
callbackUrls: {
authenticate: 'http://localhost:3000/auth/magic-link/authenticate',
resetPassword: 'http://localhost:3000/auth/reset-password/reset',
},
},

启动应用:

Terminal window
pnpm start:dev

reset-password 功能新增两个端点:

Method Path Description
POST /auth/reset-password/send 发送密码重置验证
POST /auth/reset-password/reset 重置密码

用 OTP 重置密码

使用 OTP 验证策略,发送验证目标,然后连同新密码一起提交验证凭证:

POST /auth/reset-password/send
Content-Type: application/json
{
"target": "[email protected]",
"strategy": "otp",
"method": "email"
}

邮件中包含用于密码重置流程的 OTP:

Subject: Your OTP code to reset your password
Your OTP code is: 127419

提交 target、验证码和新密码:

POST /auth/reset-password/reset
Content-Type: application/json
{
"target": "[email protected]",
"strategy": "otp",
"proof": "127419",
"newPassword": "new-password"
}

OTP 在校验时需要 target,如果省略,请求会失败并返回 Target is required for OTP verification。

用 magic link 重置密码

使用 magic link 验证策略,链接中的令牌会被用作验证凭证:

POST /auth/reset-password/send
Content-Type: application/json
{
"target": "[email protected]",
"strategy": "magic-link",
"method": "email"
}

邮件中包含类似这样的链接:

Subject: Your magic link to reset your password
Click the link to continue: http://localhost:3000/auth/reset-password/reset?token=<magic-link-token>

在这个纯后端的示例中,从链接中复制令牌,连同新密码一起提交:

POST /auth/reset-password/reset
Content-Type: application/json
{
"strategy": "magic-link",
"proof": "<magic-link-token>",
"newPassword": "new-password"
}

magic link 验证在重置请求中不需要 target,因为已验证的 target 会从令牌中还原出来。

密码重置成功后,该用户已有的会话都会被撤销。