如何使用Angular 7结合WebAPI实现带邮件重置链接的密码重置功能?
Angular 7 + WebAPI 实现密码重置功能完整指南
嘿,我之前刚好做过这个场景的实现,给你梳理一下完整的落地流程,分后端WebAPI(以ASP.NET WebAPI为例)和前端Angular 7两部分来讲,代码都是可直接参考的:
一、后端WebAPI部分
1. 生成重置令牌并发送邮件接口
首先需要一个接口接收用户邮箱,验证用户存在后生成加密的重置令牌,再把带令牌的重置链接发送到用户邮箱。
[Route("api/account/forgot-password")] [HttpPost] public async Task<IHttpActionResult> ForgotPassword(ForgotPasswordModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = await UserManager.FindByEmailAsync(model.Email); if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id))) { // 这里不要暴露用户是否存在,返回统一提示 return Ok(new { Message = "如果邮箱存在,重置链接已发送" }); } // 生成密码重置令牌 var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id); // 对令牌做URL编码,避免特殊字符破坏链接 var encodedToken = Uri.EscapeDataString(token); var encodedEmail = Uri.EscapeDataString(model.Email); // 构建前端重置页面链接 var resetLink = $"https://your-angular-app.com/reset-password?token={encodedToken}&email={encodedEmail}"; // 发送邮件(这里用SmtpClient示例,实际可换第三方邮件服务) var mailMessage = new MailMessage("noreply@yourapp.com", model.Email) { Subject = "密码重置请求", Body = $"点击链接重置密码:<a href='{resetLink}'>{resetLink}</a>", IsBodyHtml = true }; using (var smtp = new SmtpClient("your-smtp-server")) { await smtp.SendMailAsync(mailMessage); } return Ok(new { Message = "如果邮箱存在,重置链接已发送" }); } // 对应的模型类 public class ForgotPasswordModel { [Required] [EmailAddress] public string Email { get; set; } }
2. 验证令牌并重置密码接口
这个接口接收令牌、邮箱和新密码,验证令牌有效性后完成密码重置。
[Route("api/account/reset-password")] [HttpPost] public async Task<IHttpActionResult> ResetPassword(ResetPasswordModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = await UserManager.FindByEmailAsync(model.Email); if (user == null) { // 同样不暴露用户不存在的信息 return Ok(new { Message = "密码重置完成" }); } // 验证令牌并重置密码 var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword); if (result.Succeeded) { return Ok(new { Message = "密码重置成功" }); } // 返回错误信息 return BadRequest(string.Join(", ", result.Errors)); } // 对应的模型类 public class ResetPasswordModel { [Required] [EmailAddress] public string Email { get; set; } [Required] public string Token { get; set; } [Required] [StringLength(100, ErrorMessage = "{0} 至少需要 {2} 个字符长度。", MinimumLength = 6)] [DataType(DataType.Password)] public string NewPassword { get; set; } }
二、前端Angular 7部分
1. 创建「忘记密码」组件(ForgotPasswordComponent)
这个组件让用户输入邮箱,提交请求到后端发送重置链接。
模板(forgot-password.component.html)
<div class="container"> <h2>忘记密码</h2> <form [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label>邮箱</label> <input type="email" formControlName="email" class="form-control" [ngClass]="{'is-invalid': submitted && f.email.errors}"> <div *ngIf="submitted && f.email.errors" class="invalid-feedback"> <div *ngIf="f.email.errors.required">邮箱不能为空</div> <div *ngIf="f.email.errors.email">请输入有效的邮箱地址</div> </div> </div> <button type="submit" class="btn btn-primary" [disabled]="loading"> <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> 发送重置链接 </button> <div *ngIf="message" class="alert alert-info mt-3">{{ message }}</div> <div *ngIf="error" class="alert alert-danger mt-3">{{ error }}</div> </form> </div>
组件逻辑(forgot-password.component.ts)
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-forgot-password', templateUrl: './forgot-password.component.html' }) export class ForgotPasswordComponent implements OnInit { forgotPasswordForm: FormGroup; submitted = false; loading = false; message: string; error: string; constructor(private formBuilder: FormBuilder, private http: HttpClient) { } ngOnInit() { this.forgotPasswordForm = this.formBuilder.group({ email: ['', [Validators.required, Validators.email]] }); } // 快捷获取表单控件 get f() { return this.forgotPasswordForm.controls; } onSubmit() { this.submitted = true; this.message = ''; this.error = ''; // 如果表单无效,停止提交 if (this.forgotPasswordForm.invalid) { return; } this.loading = true; this.http.post('/api/account/forgot-password', this.forgotPasswordForm.value) .subscribe( (response: any) => { this.message = response.Message; this.loading = false; }, (err) => { this.error = err.error.Message || '请求失败,请稍后重试'; this.loading = false; } ); } }
2. 创建「重置密码」组件(ResetPasswordComponent)
这个组件接收URL中的令牌和邮箱参数,让用户输入新密码完成重置。
路由配置(app-routing.module.ts)
import { Routes, RouterModule } from '@angular/router'; import { ResetPasswordComponent } from './reset-password/reset-password.component'; import { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; const routes: Routes = [ { path: 'forgot-password', component: ForgotPasswordComponent }, { path: 'reset-password', component: ResetPasswordComponent }, // 其他路由... ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
模板(reset-password.component.html)
<div class="container"> <h2>重置密码</h2> <form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label>新密码</label> <input type="password" formControlName="newPassword" class="form-control" [ngClass]="{'is-invalid': submitted && f.newPassword.errors}"> <div *ngIf="submitted && f.newPassword.errors" class="invalid-feedback"> <div *ngIf="f.newPassword.errors.required">新密码不能为空</div> <div *ngIf="f.newPassword.errors.minlength">密码至少需要6个字符</div> </div> </div> <div class="form-group"> <label>确认密码</label> <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{'is-invalid': submitted && (f.confirmPassword.errors || resetPasswordForm.errors?.mismatch)}"> <div *ngIf="submitted && (f.confirmPassword.errors || resetPasswordForm.errors?.mismatch)" class="invalid-feedback"> <div *ngIf="f.confirmPassword.errors.required">确认密码不能为空</div> <div *ngIf="resetPasswordForm.errors?.mismatch">两次输入的密码不一致</div> </div> </div> <button type="submit" class="btn btn-primary" [disabled]="loading"> <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> 重置密码 </button> <div *ngIf="message" class="alert alert-success mt-3">{{ message }}</div> <div *ngIf="error" class="alert alert-danger mt-3">{{ error }}</div> </form> </div>
组件逻辑(reset-password.component.ts)
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; // 自定义验证器:检查两次密码是否一致 function matchPassword(control: AbstractControl): ValidationErrors | null { const password = control.get('newPassword')?.value; const confirmPassword = control.get('confirmPassword')?.value; return password === confirmPassword ? null : { mismatch: true }; } @Component({ selector: 'app-reset-password', templateUrl: './reset-password.component.html' }) export class ResetPasswordComponent implements OnInit { resetPasswordForm: FormGroup; submitted = false; loading = false; message: string; error: string; private token: string; private email: string; constructor( private formBuilder: FormBuilder, private http: HttpClient, private route: ActivatedRoute ) { } ngOnInit() { // 从URL参数中获取令牌和邮箱 this.token = this.route.snapshot.queryParamMap.get('token'); this.email = this.route.snapshot.queryParamMap.get('email'); this.resetPasswordForm = this.formBuilder.group({ newPassword: ['', [Validators.required, Validators.minLength(6)]], confirmPassword: ['', Validators.required] }, { validators: matchPassword }); } get f() { return this.resetPasswordForm.controls; } onSubmit() { this.submitted = true; this.message = ''; this.error = ''; if (this.resetPasswordForm.invalid) { return; } this.loading = true; // 组装请求参数 const payload = { email: this.email, token: this.token, newPassword: this.f.newPassword.value }; this.http.post('/api/account/reset-password', payload) .subscribe( (response: any) => { this.message = response.Message; this.loading = false; }, (err) => { this.error = err.error.Message || '密码重置失败,请检查链接是否有效'; this.loading = false; } ); } }
三、关键注意事项
- 令牌有效期:建议给重置令牌设置有效期(比如1小时),可以在生成令牌时配置,避免令牌长期有效带来安全风险。
- URL编码:一定要对令牌和邮箱做URL编码,因为令牌中可能包含
+、/等特殊字符,直接放在URL中会被解析错误。 - 安全提示:生产环境一定要用HTTPS传输,避免令牌被窃取;邮件中的链接也要用HTTPS。
- 错误提示:不要暴露用户是否存在的信息(比如忘记密码时,不管邮箱是否存在都返回相同提示),防止恶意用户枚举邮箱。
内容的提问来源于stack exchange,提问作者Pushpendra Tripathi
相关产品推荐
相关产品推荐

