用Abp实现找回密码和密码强制过期策略

用户找回密码,确切地说是重置密码,为了保证用户账号安全,原始密码将不再以明文的方式找回,而是通过短信或者邮件的方式发送一个随机的重置校验码(带校验码的页面连接),用户点击该链接,跳转到重置密码页面,输入新的密码。这个重置校验码是一次性的,用户重置密码后立即失效。

用户找回密码是在用户没有登录时进行的,因此需要先校验身份(除用户名+密码外的第二种身份验证方式)。
第二种身份验证的前提是绑定了手机号或者邮箱,如果没有绑定,那么只能通过管理员进行原始密码重置。

密码强制过期策略,是指用户在一段时间内没有修改密码,在下次登录时系统阻止用户登录,直到用户修改了密码后方可继续登录。此策略提高用户账号的安全性。

在这里插入图片描述

找回密码和密码过期重置密码,两种机制有相近的业务逻辑,即密码重置。今天我们来实现这个功能。

重置密码

Abp框架中,AbpUserBase类中已经定义了重置校验码PasswordResetCode属性,以及SetNewPasswordResetCode方法,用于生成新的重置校验码。

[StringLength(328)]
public virtual string PasswordResetCode { get; set; }
public virtual void SetNewPasswordResetCode()
{PasswordResetCode = Guid.NewGuid().ToString("N").Truncate(328);
}

在UserAppService中添加ResetPasswordByCode,用于响应重置密码的请求。
在其参数ResetPasswordByLinkDto中携带了校验信息PasswordResetCode,因此添加了特性[AbpAllowAnonymous],不需要登录认证即可调用此接口

密码更新完成后,立刻将PasswordResetCode重置为null,以防止重复使用。

[AbpAllowAnonymous]
public async Task<bool> ResetPasswordByCode(ResetPasswordByLinkDto input)
{await _userManager.InitializeOptionsAsync(AbpSession.TenantId);var currentUser = await _userManager.GetUserByIdAsync(input.UserId);if (currentUser == null || currentUser.PasswordResetCode.IsNullOrEmpty() || currentUser.PasswordResetCode != input.ResetCode){throw new UserFriendlyException("PasswordResetCode不正确");}var loginAsync = await _logInManager.LoginAsync(currentUser.UserName, input.NewPassword, shouldLockout: false);if (loginAsync.Result == AbpLoginResultType.Success){throw new UserFriendlyException("重置的密码不应与之前密码相同");}if (currentUser.IsDeleted || !currentUser.IsActive){return false;}CheckErrors(await _userManager.ChangePasswordAsync(currentUser, input.NewPassword));currentUser.PasswordResetCode = null;currentUser.LastPasswordModificationTime = DateTime.Now;await this._userManager.UpdateAsync(currentUser);return true;
}

找回密码

发送验证码

使用AbpBoilerplate.Sms作为短信服务库。

之前的项目中,我们定义好了ICaptchaManager接口,已经实现了验证码的发送、验证码校验、解绑手机号、绑定手机号

这4个功能,通过定义用途(purpose)字段以校验区分短信模板

public interface ICaptchaManager
{Task BindAsync(string token);Task UnbindAsync(string token);Task SendCaptchaAsync(long userId, string phoneNumber, string purpose);Task<bool> VerifyCaptchaAsync(string token, string purpose = "IDENTITY_VERIFICATION");
}

添加一个用于重置密码的purpose,在CaptchaPurpose枚举类型中添加RESET_PASSWORD

public class CaptchaPurpose
{...public const string RESET_PASSWORD = "RESET_PASSWORD";}

在SMS服务商管理端后台申请一个短信模板,用于重置密码。

在这里插入图片描述

打开短信验证码的领域服务类SmsCaptchaManager, 添加RESET_PASSWORD对应短信模板的编号

public async Task SendCaptchaAsync(long userId, string phoneNumber, string purpose)
{var captcha = CommonHelper.GetRandomCaptchaNumber();var model = new SendSmsRequest();model.PhoneNumbers = new string[] { phoneNumber };model.SignName = "MatoApp";model.TemplateCode = purpose switch{...CaptchaPurpose.RESET_PASSWORD => "SMS_1587660"    //添加重置密码对应短信模板的编号};...
}

接下来我们创建ResetPasswordManager类,用于处理找回密码和密码过期重置密码的业务逻辑。
注入UserManager,ISmsService,SmsCaptchaManager,EmailCaptchaManager。

public class ResetPasswordManager : ITransientDependency
{private readonly UserManager userManager;private readonly ISmsService smsService;private readonly SmsCaptchaManager smsCaptchaManager;private readonly EmailCaptchaManager emailCaptchaManager;public ResetPasswordManager(UserManager userManager,ISmsService smsService,SmsCaptchaManager smsCaptchaManager,EmailCaptchaManager emailCaptchaManager){this.userManager = userManager;this.smsService = smsService;this.smsCaptchaManager = smsCaptchaManager;this.emailCaptchaManager = emailCaptchaManager;}

在ResetPasswordManager中添加SendForgotPasswordCaptchaAsync方法,用于短信或邮箱方式的身份验证。

public async Task SendForgotPasswordCaptchaAsync(string provider, string phoneNumberOrEmail)
{User user;if (provider == "Email"){user = await userManager.FindByEmailAsync(phoneNumberOrEmail);if (user == null){throw new UserFriendlyException("未找到绑定邮箱的用户");}await emailCaptchaManager.SendCaptchaAsync(user.Id, user.EmailAddress, CaptchaPurpose.RESET_PASSWORD);}else if (provider == "Phone"){user = await userManager.FindByNameOrPhoneNumberAsync(phoneNumberOrEmail);if (user == null){throw new UserFriendlyException("未找到绑定手机号的用户");}await smsCaptchaManager.SendCaptchaAsync(user.Id, user.PhoneNumber, CaptchaPurpose.RESET_PASSWORD);}}

校验验证码

添加VerifyAndSendResetPasswordLinkAsync方法,用于校验验证码,并发送重置密码的链接。

public async Task VerifyAndSendResetPasswordLinkAsync(string token, string provider)
{if (provider == "Email"){EmailCaptchaTokenCacheItem currentItem = await emailCaptchaManager.GetToken(token);if (currentItem == null || currentItem.Purpose != CaptchaPurpose.RESET_PASSWORD){throw new UserFriendlyException("验证码不正确或已过期");}var user = await userManager.GetUserByIdAsync(currentItem.UserId);var emailAddress = currentItem.EmailAddress;await SendEmailResetPasswordLink(user, emailAddress);await emailCaptchaManager.RemoveToken(token);}else if (provider == "Phone"){SmsCaptchaTokenCacheItem currentItem = await smsCaptchaManager.GetToken(token);if (currentItem == null || currentItem.Purpose != CaptchaPurpose.RESET_PASSWORD){throw new UserFriendlyException("验证码不正确或已过期");}var user = await userManager.GetUserByIdAsync(currentItem.UserId);var phoneNumber = currentItem.PhoneNumber;await SendSmsResetPasswordLink(user, phoneNumber);await smsCaptchaManager.RemoveToken(token);}else{throw new UserFriendlyException("验证码提供者错误");}}

发送重置密码链接

创建SendSmsResetPasswordLink,用于对当前用户产生一个NewPasswordResetCode,并发送重置密码的短信链接。

private async Task SendSmsResetPasswordLink(User user, string phoneNumber)
{var model = new SendSmsRequest();user.SetNewPasswordResetCode();var passwordResetCode = user.PasswordResetCode;model.PhoneNumbers = new string[] { phoneNumber };model.SignName = "MatoApp";model.TemplateCode = "SMS_255330989";//for aliyunmodel.TemplateParam = JsonConvert.SerializeObject(new { username = user.UserName, code = passwordResetCode });//for tencent-cloud//model.TemplateParam = JsonConvert.SerializeObject(new string[] { user.UserName, passwordResetCode });var result = await smsService.SendSmsAsync(model);if (string.IsNullOrEmpty(result.BizId) && result.Code != "OK"){throw new UserFriendlyException("验证码发送失败,错误信息:" + result.Message);}
}

创建接口

在UserAppService暴露出SendForgotPasswordCaptcha和VerifyAndSendResetPasswordLink两个接口,

注意这两个接口都需要添加[AbpAllowAnonymous]特性,因为在用户未登录的情况下,也需要使用这两个接口。

[AbpAllowAnonymous]
public async Task SendForgotPasswordCaptcha(ForgotPasswordProviderDto input)
{var provider = input.Provider;var phoneNumberOrEmail = input.ProviderNumber;await forgotPasswordManager.SendForgotPasswordCaptchaAsync(provider, phoneNumberOrEmail);}[AbpAllowAnonymous]
public async Task VerifyAndSendResetPasswordLink(SendResetPasswordLinkDto input)
{var provider = input.Provider;var token = input.Token;await forgotPasswordManager.VerifyAndSendResetPasswordLinkAsync(token, provider);}

这两个接口分别在用户忘记密码的两个阶段调用,

  1. 第一阶段是发送验证码,
  2. 第二阶段是校验验证码并发送重置密码的链接。

在这里插入图片描述

密码强制过期策略

在User实体中添加一个属性,用于记录密码最后修改时间,在登录时验证这个时间至此时的时间跨度,如果超过一定时间(例如90天),强制用户重置密码。

[Required]
public DateTime LastPasswordModificationTime { get; set; }

改写接口

将重置校验码PasswordResetCode添加到AuthenticateResultModel中

public string PasswordResetCode { get; set; }

打开TokenAuthController,注入ResetPasswordManager服务对象

登录验证终节点方法Authenticate中,添加对密码强制过期的逻辑代码

[HttpPost]
public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model)
{var loginResult = await GetLoginResultAsync(model.UserNameOrEmailAddress,model.Password,GetTenancyNameOrNull());...//Password Expiration Checkif (DateTime.Now - loginResult.User.LastPasswordModificationTime > TimeSpan.FromDays(90)){loginResult.User.SetNewPasswordResetCode();return new AuthenticateResultModel{PasswordResetCode = loginResult.User.PasswordResetCode,UserId = loginResult.User.Id,};}}

当登录账号的LastPasswordModificationTime距此时大于90天时,将阻止登录,并提示账户密码已过期,需要修改密码
 

在这里插入图片描述

在这里插入图片描述

Vue网页端开发

重置密码页面

创建Web端的重置密码页面,用于用户重置密码。

在这里插入图片描述

当用户通过短信或邮箱接收到重置密码的链接后,点击链接,会跳转到重置密码的页面,用户输入新密码后,点击提交,就可以完成密码重置。

连接格式如下

http://localhost:8080/reset-password-sample/reset.html?code=f16b5fbb057d4a04bce5b9e7f24e1d56&userId=1

项目参与实际生产中请加密参数,在此为了简单起见采用明文传递。

<template><div id="app"><div class="title-container center"><h3 class="title">修改密码</h3></div><el-row><el-formref="loginForm":model="input"class="login-form"autocomplete="on"label-position="left"><el-form-item label="验证码"><el-input v-model="input.code" placeholder="请输入验证码" clearable /></el-form-item><el-form-item label="新密码" prop="newPassword"><el-inputv-model="input.newPassword"placeholder="请输入新密码"clearableshow-password/></el-form-item><el-form-item label="新密码(确认)" prop="newPassword2"><el-inputv-model="input.newPassword2"placeholder="请再次输入新密码"clearableshow-password/></el-form-item><el-row type="flex" class="row-bg"><el-col :offset="6" :span="10"><el-buttontype="primary"style="width: 100%"@click.native.prevent="submit">修改</el-button></el-col></el-row></el-form></el-row></div>
</template>

创建页面时会根据url中的参数,获取code和userId。

created: async function () {var url = window.location.href;var reg = /[?&]([^?&#]+)=([^?&#]+)/g;var param = {};var ret = reg.exec(url);while (ret) {param[ret[1]] = ret[2];ret = reg.exec(url);}if ("code" in param) {this.input.code = param["code"];}if ("userId" in param) {this.input.userId = param["userId"];}},

点击修改时会触发submit方法,这个方法会调用ResetPasswordByCode接口,将UserId,newPassword以及resetCode回传。

 async submit() {if ((this.input.newPassword != this.input.newPassword2) == null) {this.$message.warning("两次输入的密码不一致!");return;}await request(`${this.host}${this.prefix}/User/ResetPasswordByCode`,"post",{userId: this.input.userId,newPassword: this.input.newPassword,resetCode: this.input.code,}).catch((re) => {var res = re.response.data;this.errorMessage(res.error.message);}).then(async (res) => {var data = res.data.result;this.successMessage("密码修改成功!");window.location.href = "/reset-password-sample.html";}).finally(() => {setTimeout(() => {this.loading = false;}, 1.5 * 1000);});},

忘记密码控件

在登录页面中,添加忘记密码的控件。

在这里插入图片描述

resetPasswordStage 是判定当前是哪个阶段的变量,
0表示正常用户名密码登录(初始状态),1表示输入手机号或邮箱验证身份,2表示通过验证即将发送重置密码的链接。

默认两种方式,一种是短信验证码,一种是邮箱验证码,这里我们采用了elementUI的tab组件,来实现两种方式的切换。

<template v-else-if="resetPasswordStage == 1"><p>请输入与要找回的账户关联的手机号或邮箱。我们将为你发送密码重置连接</p><el-tabs tab-position="top" v-model="forgotPasswordProvider.provider"><el-tab-pane :lazy="true" label="通过手机号找回" name="Phone"><el-row><el-col :span="24"><el-inputv-model="forgotPasswordProvider.providerNumber":placeholder="'请输入手机号'"tabindex="2"><el-buttonslot="append"@click="sendResetPasswordLink":disabled="forgotPasswordProvider.providerNumber == ''">下一步</el-button></el-input></el-col></el-row></el-tab-pane><el-tab-pane :lazy="true" label="通过邮箱找回" name="Email"><el-row><el-col :span="24"><el-alertv-if="showResetRequireSuccess"title="密码重置连接已发送至登录用户对应的邮箱,请查收"type="info"></el-alert></el-col><el-col :span="24"><p>建设中..</p></el-col></el-row></el-tab-pane></el-tabs>
</template>

不通的阶段,将分别调用不同的接口,sendResetPasswordLink以及verifyAndSendResetPasswordLink。

调用verifyAndSendResetPasswordLink接口完毕时,resetPasswordStage将设置位初始状态,即0。

async sendResetPasswordLink() {await request(`${this.host}${this.prefix}/User/SendForgotPasswordCaptcha`,"post",this.forgotPasswordProvider).catch((re) => {var res = re.response.data;this.errorMessage(res.error.message);}).then(async (re) => {if (re) {this.successMessage("发送验证码成功");this.resetPasswordStage++;}});
},
async verifyAndSendResetPasswordLink() {await request(`${this.host}${this.prefix}/User/VerifyAndSendResetPasswordLink`,"post",{provider: this.forgotPasswordProvider.provider,token: this.captchaToken,}).catch((re) => {var res = re.response.data;this.errorMessage(res.error.message);}).then(async (re) => {if (re) {this.successMessage("发送连接成功");this.resetPasswordStage = 0;}});
},

密码过期提示

主页面中添加对passwordResetCode的响应,当passwordResetCode不为空时,显示一个提示框,提示用户密码已超过90天未修改,请修改密码。

<el-alertv-if="passwordResetCode != null"close-text="点此修改密码"title="密码已超过90天未修改,为了安全,请修改密码"type="info"@close="gotoUrl('/reset-password-sample/reset.html?code=' +passwordResetCode +'&userId=' +userId)">
</el-alert>

在这里插入图片描述


用户点击点此修改密码按钮时将跳转至重置密码页面。 

项目地址

Github:matoapp-samples

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/23798.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot 日志文件

一、日志的作用 日志是程序的重要组成部分&#xff0c;想象一下&#xff0c;如果程序报错了&#xff0c;不让你打开控制台看日志&#xff0c;那么你能找到报错的原因吗 答案是否定的&#xff0c;写程序不是买彩票&#xff0c;不能完全靠猜&#xff0c;因此日志对于我们来说&a…

C语言数组笔试题(详解)

目录 插入知识&#xff1a; 一.指向函数指针数组的指针 二.回调函数 什么是回调函数&#xff1f; 三.数组笔试题 个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生&#x1f43b;‍❄个人主页&#xff1a;GOTXX &#x1f4…

【Apollo学习笔记】—— 相机仿真

文章目录 前言相关代码整理 测试实践文件目录包管理BUILD文件以及cyberfile.xml文件源程序BUILD运行结果其他参考CameraOutput channels启动camera驱动启动camera video compression驱动 前言 本文是对Cyber RT的学习记录,文章可能存在不严谨、不完善、有缺漏的部分&#xff0…

[迁移学习]领域泛化

一、概念 相较于领域适应&#xff0c;领域泛化(Domain generalization)最显著的区别在于训练过程中不能访问测试集。 领域泛化的损失函数一般可以描述为以下形式&#xff1a; 该式分为三项&#xff1a;第一项表示各训练集权重的线性组合&#xff0c;其中π为使该项最小的系数&a…

react工程化配置

道阻且长&#xff0c;行而不辍&#xff0c;未来可期 1.安装react yarn create react-app demo --template typescript cd demo yarn start2.配置蓝图模版 2.1安装blueprint插件 https://github.com/shredor/blueprint-templates-cli#readme yarn add blueprint-templates-c…

WAF绕过-漏洞利用篇-sql注入+文件上传-过狗

WAF绕过主要集中在信息收集&#xff0c;漏洞发现&#xff0c;漏洞利用&#xff0c;权限控制四个阶段。 1、什么是WAF&#xff1f; Web Application Firewall&#xff08;web应用防火墙&#xff09;&#xff0c;一种公认的说法是“web应用防火墙通过执行一系列针对HTTP/HTTPS的安…

webpack基础知识六:说说webpack的热更新是如何做到的?原理是什么?

一、是什么 HMR全称 Hot Module Replacement&#xff0c;可以理解为模块热替换&#xff0c;指在应用程序运行过程中&#xff0c;替换、添加、删除模块&#xff0c;而无需重新刷新整个应用 例如&#xff0c;我们在应用运行过程中修改了某个模块&#xff0c;通过自动刷新会导致…

【C#学习笔记】装箱和拆箱

文章目录 装箱和拆箱性能消耗装箱拆箱 比较var&#xff0c;object&#xff0c;dynamic&#xff0c;\<T\>varobject\<T\> 泛型dynamic 装箱和拆箱 在讲引用类型object的时候&#xff0c;我们说它是万能的&#xff0c;却没说它万能在哪里。 除了object为每一种变量…

Grafana集成prometheus(4.Grafana添加预警)

上文已经完成了grafana对prometheus的集成及数据导入&#xff0c;本文主要记录grafana的预警功能&#xff08;以内存为例&#xff09; 添加预警 添加入口&#xff08;2个&#xff09; databorard面板点击edit&#xff0c;下方有个Alert的tab&#xff0c;创建Alert rules依赖…

ffmpeg + nginx 实现rtsp视频流转m3u8视频流,转码推流(linux)

FFmpeg即是一款音视频编解码工具&#xff0c;同时也是一组音视频编码开发套件&#xff0c;作为编码开发套件&#xff0c;它为开发者提供了丰富的音视频处理的调用接口。 FFmpeg提供了多种媒体格式的封装和解封装&#xff0c;包括多种音视频编码、多种协议的流媒体、多种多彩格式…

苹果电脑第三方应用程序卸载工具CleanMyMacX4.14

CleanMyMacX&#xff0c;这是一款可以帮助你快速识别并卸载电脑上的多个应用程序的第三方工具&#xff0c;省去了逐个卸载的繁琐步骤。 我们首先要到下载CleanMyMacX&#xff0c; CleanMyMac X全新版下载如下: https://wm.makeding.com/iclk/?zoneid49983 然后&#xff0c…

【项目 计网2】4.4网络模型 4.5协议 4.6网络通信的过程

文章目录 4.4网络模型OSI七层参考模型TCP/IP四层模型&#xff08;常用&#xff09;简介四层介绍 4.5协议简介常见协议UDP协议TCP协议IP协议以太网帧协议&#xff08;MAC地址封装&#xff09;ARP协议&#xff08;IP->MAC&#xff09; 4.6网络通信的过程封装分用 4.4网络模型 …

Stable Diffusion 硬核生存指南:WebUI 中的 GFPGAN

本篇文章聊聊 Stable Diffusion WebUI 中的核心组件&#xff0c;强壮的人脸图像面部画面修复模型 GFPGAN 相关的事情。 写在前面 本篇文章的主角是开源项目 TencentARC/GFPGAN&#xff0c;和上一篇文章《Stable Diffusion 硬核生存指南&#xff1a;WebUI 中的 CodeFormer》提…

k8s存储卷

目录 一、为什么要存储卷&#xff1f;二、emptyDir存储卷三、hostPath存储卷四、 nfs共享存储卷五、PVC 和 PV5.1 PV和PVC之间的相互作用遵循的生命周期5.2 PV 的状态5.3 一个PV从创建到销毁的具体流程 六、静态创建pv和pvc资源由pod运用过程6.1 在NFS主机上创建共享目录&#…

03 制作Ubuntu启动盘

1 软碟通 我是用软碟通制作启动盘。安装软碟通时一定要把虚拟光驱给勾选上&#xff0c;其余两个可以看你心情。 2 镜像文件 我使用清华镜像网站找到的Ubuntu镜像文件。 Index of /ubuntu-releases/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 请自己选择镜像…

K8S系列文章之 使用Kind部署K8S 并发布服务

简单介绍 kind 即 Kubernetes In Docker&#xff0c;顾名思义&#xff0c;就是将 k8s 所需要的所有组件&#xff0c;全部部署在一个docker容器中&#xff0c;是一套开箱即用的 k8s 环境搭建方案。使用 kind 搭建的集群无法在生产中使用&#xff0c;但是如果你只是想在本地简单…

队列中offer,add;poll,remove;peek,element之间的区别

offer和add的区别 offer() 和 add() 都是向队列中加入新项。 一些队列有大小限制&#xff0c;因此如果想在一个满的队列中加入一个新项&#xff0c;多出的项就会被拒绝。 这时新的offer方法就可以起作用了。它不是对调用add()方法抛出一个unchecked异常&#xff0c;而只是得…

初阶C语言——特别详细地介绍函数

系列文章目录 第一章 “C“浒传——初识C语言&#xff08;更适合初学者体质哦&#xff01;&#xff09; 第二章 详细认识分支语句和循环语句以及他们的易错点 第三章 初阶C语言——特别详细地介绍函数 目录 系列文章目录 前言 一、函数是个什么鬼东西&#xff1f; 二、C语…

css中的bfc是什么?

什么bfc&#xff1f; BFC&#xff08;Block Formatting Context&#xff09;块级 格式化 上下文。 BFC就是页面上的一个隔离的独立盒子&#xff0c;容器里面的子元素和外面的元素不会相互影响。 为什么要bfc? bfc是我们去主动触发的,并不是自动就存在的,它是帮助我们解决cs…

【雕爷学编程】MicroPython动手做(28)——物联网之Yeelight 2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…