用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.rhkb.cn/news/77415.html

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

相关文章

HTML 基础标签

前言 当今互联网时代&#xff0c;网页是我们获取信息、交流和展示自己的重要渠道之一。而HTML&#xff08;超文本标记语言&#xff09;作为构建网页的基础&#xff0c;学习掌握HTML标签成为了必不可少的技能。 标题标签 <h1>~<h6>&#xff1a;这是用来定义标题的…

VisualStudioWindows下 远程调试

前置条件 1、调试方与被调试方&#xff0c;以下简称调试方为A&#xff0c;被调试方为B。A与B双方能相互ping通 2、B需要运行RemoteDebugger服务&#xff0c;该程序位于C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Remote Debugger下。 我这里是安装…

选择结构的学习

选择结构 思考以下问题&#xff1a; 常用的逻辑运算符及其作用&#xff1f; 请写出判断分数大于 60 并且分数小于 100 的表达式 if-else 选择结构执行的顺序是什么&#xff1f; 多重 if 选择结构的执行流程是怎样的&#xff1f; if 选择结构书写规范有哪些&#xff1f; 通过下…

设计模式、Java8新特性实战 - List<T> 抽象统计组件

一、背景 在日常写代码的过程中&#xff0c;针对List集合&#xff0c;统计里面的某个属性&#xff0c;是经常的事情&#xff0c;针对List的某个属性的统计&#xff0c;我们目前大部分时候的代码都是这样写&#xff0c;每统计一个变量&#xff0c;就要定义一个值&#xff0c;且…

刷脸登录(人工智能)

刷脸登录 理解刷脸登录的需求 理解刷脸登录的开发流程实现刷脸登录功能 浅谈人工智能 人工智能的概述 人工智能&#xff08;Artificial Intelligence&#xff09;&#xff0c;英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门…

渗透-01:DNS原理和HTML字符编码-HTML实体编码

一、DNS概念 DNS (Domain Name System 的缩写)就是根据域名查出IP地址(常用) DNS分类&#xff1a; 正向解析&#xff1a;已知域名解析IP反向解析&#xff1a;已知IP解析对应的域名 二、查询过程 工具软件dig可以显示整个查询过程 [rootnode01 ~]# dig baidu.com; <<>&…

【项目 计网3】Socket介绍 4.9字节序 4.10字节序转换函数

文章目录 4.8 Socket介绍4.9字节序简介字节序举例 4.10字节序转换函数 4.8 Socket介绍 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进…

windows开机运行jar

windows开机自启动jar包&#xff1a; 一、保存bat批处理文件 echo off %1 mshta vbscript:CreateObject("WScript.Shell").Run("%~s0 ::",0,FALSE)(window.close)&&exit java -jar E:\projects\ruoyi-admin.jar > E:\server.log 2>&1 &…

测试平台——项目模块模型类设计

这里写目录标题 一、项目应用1、项目包含接口&#xff1a;2、创建子应用3、项目模块设计a、模型类设计b、序列化器类设计c、视图类设计 4、接口模块设计a、模型类设计b、序列化器类设计c、视图类设计 5、环境模块设计6、DRF中的通用过滤6.1、设置过滤器后端 一、项目应用 1、项…

GEE:谐波模型在遥感影像中的应用(季节性变化的拟合与可视化)

作者:CSDN @ _养乐多_ 谐波模型是一种常用的工具,用于拟合和分析影像数据中的周期性和季节性变化。本文将介绍如何使用Google Earth Engine平台实现谐波模型,通过对Landsat影像进行处理和拟合,展示季节性变化的拟合结果,并通过图表和地图可视化展示数据。 谐波模型是一种…

LabVIEW开发多材料摩擦电测量控制系统

LabVIEW开发多材料摩擦电测量控制系统 摩擦电效应是两个物体摩擦在一起&#xff0c;电荷从一个物体转移到另一个物体的现象&#xff0c;从而导致两个物体携带相等和相反的电荷。接触和充电是主导该过程的两个关键因素。当静电荷累积到一定水平时&#xff0c;可能会出现放电现象…

一起学算法(选择排序篇)

距离上次更新已经很久了&#xff0c;以前都是非常认真的写笔记进行知识分享&#xff0c;但是带来的情况并不是很好&#xff0c;一度认为发博客是没有意义的&#xff0c;但是这几天想了很多&#xff0c;已经失去了当时写博客的初心了&#xff0c;但是我觉得应该做点有意义的事&a…

sql 参数自动替换

需求&#xff1a;看日志时&#xff0c;有的sql 非常的长&#xff0c;参数比较多&#xff0c;无法直接在sql 客户端工具执行&#xff0c;如果一个一个的把问号占位符替换为参数太麻烦&#xff0c;因此写个html 小工具&#xff0c;批量替换&#xff1a; 代码&#xff1a; <!…

【项目 线程4】3.12生产者消费者模型 3.13条件变量 3.14信号量 C++实现生产者消费者模型

3.12生产者消费者模型 生产者消费者模型中的对象&#xff1a; 1、生产者 2、消费者 3、容器 若容器已满&#xff0c;生产者阻塞在这&#xff0c;通知消费者去消费&#xff1b;若容器已空&#xff0c;则消费者阻塞&#xff0c;通知生产者去生产。生产者可以有多个&#xff0c;消…

HTML5中的data-*属性

介绍&#xff1a; data-*全局属性是一类被称为自定义数据属性的属性&#xff0c;它赋予我们在所有 HTML 元素上嵌入自定义数据属性的能力。 data-*的使用 <div class"child" data-name"小红" data-age"18"></div> 在js里有两种获…

JavaEE——网络初识 (简单介绍两种协议以及网络通信的基础概念)

文章目录 一、简单了解网络发展二、网络通信基础认识三、利用UDP举例解释网络信息传输 一、简单了解网络发展 总的来讲&#xff0c;网络的发展史就是&#xff0c;先是一小部分的计算机之间连接通信&#xff0c;随着技术发展&#xff0c;逐渐扩大范围&#xff0c;形成了我们当前…

【unity】Pico VR 开发笔记(视角移动)

【unity】Pico VR 开发笔记&#xff08;视角移动&#xff09; 视角移动是简单的基础功能&#xff0c;这里区别于头显定位获得的小范围位移&#xff0c;是长距离不影响安全边界的位移方式。的常见的位移方式有两种&#xff0c;其一是触发后瞬间传送到指定位置&#xff0c;其次是…

TBB库中实现协程(coroutine)的源码说明

源码请见: https://github.com/oneapi-src/oneTBB/blob/master/src/tbb/co_context.h 在windows系统&#xff0c;TBB(也就是intel 的 oneTBB库)&#xff0c;通过windwos fiber(纤程)来实现协程(coroutine)。 创建一个协程,代码很简洁: inline void create_coroutine(corouti…

图论-简明导读

计算机图论是计算机科学中的一个重要分支&#xff0c;它主要研究图的性质和结构&#xff0c;以及如何在计算机上有效地存储、处理和操作这些图。本文将总结计算机图论的核心知识点。 一、基本概念 计算机图论中的基本概念包括图、节点、边等。图是由节点和边构成的数据结构&am…

前端技术搭建五子棋游戏(内含源码)

The sand accumulates to form a pagoda ✨ 写在前面✨ 功能介绍✨ 页面搭建✨ 样式设置✨ 逻辑部分 ✨ 写在前面 上周我们实通过前端基础实现了拼图游戏&#xff0c;今天还是继续按照我们原定的节奏来带领大家完成一个五子棋游戏&#xff0c;功能也比较简单简单&#xff0c;也…