@

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

用户找回密码是在用户没有登录时进行的,因此需要先校验身份(除用户名+密码外的第二种身份验证方式)。

第二种身份验证的前提是绑定了手机号或者邮箱,如果没有绑定,那么只能通过管理员进行原始密码重置。

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

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

重置密码

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

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

在UserAppService中添加ResetPasswordByCode,用于响应重置密码的请求。

在其参数ResetPasswordByLinkDto中携带了校验信息PasswordResetCode,因此添加了特性[AbpAllowAnonymous],不需要登录认证即可调用此接口

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

  1. [AbpAllowAnonymous]
  2. public async Task<bool> ResetPasswordByCode(ResetPasswordByLinkDto input)
  3. {
  4. await _userManager.InitializeOptionsAsync(AbpSession.TenantId);
  5. var currentUser = await _userManager.GetUserByIdAsync(input.UserId);
  6. if (currentUser == null || currentUser.PasswordResetCode.IsNullOrEmpty() || currentUser.PasswordResetCode != input.ResetCode)
  7. {
  8. throw new UserFriendlyException("PasswordResetCode不正确");
  9. }
  10. var loginAsync = await _logInManager.LoginAsync(currentUser.UserName, input.NewPassword, shouldLockout: false);
  11. if (loginAsync.Result == AbpLoginResultType.Success)
  12. {
  13. throw new UserFriendlyException("重置的密码不应与之前密码相同");
  14. }
  15. if (currentUser.IsDeleted || !currentUser.IsActive)
  16. {
  17. return false;
  18. }
  19. CheckErrors(await _userManager.ChangePasswordAsync(currentUser, input.NewPassword));
  20. currentUser.PasswordResetCode = null;
  21. currentUser.LastPasswordModificationTime = DateTime.Now;
  22. await this._userManager.UpdateAsync(currentUser);
  23. return true;
  24. }

找回密码

发送验证码

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

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

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

  1. public interface ICaptchaManager
  2. {
  3. Task BindAsync(string token);
  4. Task UnbindAsync(string token);
  5. Task SendCaptchaAsync(long userId, string phoneNumber, string purpose);
  6. Task<bool> VerifyCaptchaAsync(string token, string purpose = "IDENTITY_VERIFICATION");
  7. }

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

  1. public class CaptchaPurpose
  2. {
  3. ...
  4. public const string RESET_PASSWORD = "RESET_PASSWORD";
  5. }

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

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

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

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

注入UserManager,ISmsService,SmsCaptchaManager,EmailCaptchaManager。

  1. public class ResetPasswordManager : ITransientDependency
  2. {
  3. private readonly UserManager userManager;
  4. private readonly ISmsService smsService;
  5. private readonly SmsCaptchaManager smsCaptchaManager;
  6. private readonly EmailCaptchaManager emailCaptchaManager;
  7. public ResetPasswordManager(
  8. UserManager userManager,
  9. ISmsService smsService,
  10. SmsCaptchaManager smsCaptchaManager,
  11. EmailCaptchaManager emailCaptchaManager
  12. )
  13. {
  14. this.userManager = userManager;
  15. this.smsService = smsService;
  16. this.smsCaptchaManager = smsCaptchaManager;
  17. this.emailCaptchaManager = emailCaptchaManager;
  18. }

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

  1. public async Task SendForgotPasswordCaptchaAsync(string provider, string phoneNumberOrEmail)
  2. {
  3. User user;
  4. if (provider == "Email")
  5. {
  6. user = await userManager.FindByEmailAsync(phoneNumberOrEmail);
  7. if (user == null)
  8. {
  9. throw new UserFriendlyException("未找到绑定邮箱的用户");
  10. }
  11. await emailCaptchaManager.SendCaptchaAsync(user.Id, user.EmailAddress, CaptchaPurpose.RESET_PASSWORD);
  12. }
  13. else if (provider == "Phone")
  14. {
  15. user = await userManager.FindByNameOrPhoneNumberAsync(phoneNumberOrEmail);
  16. if (user == null)
  17. {
  18. throw new UserFriendlyException("未找到绑定手机号的用户");
  19. }
  20. await smsCaptchaManager.SendCaptchaAsync(user.Id, user.PhoneNumber, CaptchaPurpose.RESET_PASSWORD);
  21. }
  22. }

校验验证码

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

  1. public async Task VerifyAndSendResetPasswordLinkAsync(string token, string provider)
  2. {
  3. if (provider == "Email")
  4. {
  5. EmailCaptchaTokenCacheItem currentItem = await emailCaptchaManager.GetToken(token);
  6. if (currentItem == null || currentItem.Purpose != CaptchaPurpose.RESET_PASSWORD)
  7. {
  8. throw new UserFriendlyException("验证码不正确或已过期");
  9. }
  10. var user = await userManager.GetUserByIdAsync(currentItem.UserId);
  11. var emailAddress = currentItem.EmailAddress;
  12. await SendEmailResetPasswordLink(user, emailAddress);
  13. await emailCaptchaManager.RemoveToken(token);
  14. }
  15. else if (provider == "Phone")
  16. {
  17. SmsCaptchaTokenCacheItem currentItem = await smsCaptchaManager.GetToken(token);
  18. if (currentItem == null || currentItem.Purpose != CaptchaPurpose.RESET_PASSWORD)
  19. {
  20. throw new UserFriendlyException("验证码不正确或已过期");
  21. }
  22. var user = await userManager.GetUserByIdAsync(currentItem.UserId);
  23. var phoneNumber = currentItem.PhoneNumber;
  24. await SendSmsResetPasswordLink(user, phoneNumber);
  25. await smsCaptchaManager.RemoveToken(token);
  26. }
  27. else
  28. {
  29. throw new UserFriendlyException("验证码提供者错误");
  30. }
  31. }

发送重置密码链接

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

  1. private async Task SendSmsResetPasswordLink(User user, string phoneNumber)
  2. {
  3. var model = new SendSmsRequest();
  4. user.SetNewPasswordResetCode();
  5. var passwordResetCode = user.PasswordResetCode;
  6. model.PhoneNumbers = new string[] { phoneNumber };
  7. model.SignName = "MatoApp";
  8. model.TemplateCode = "SMS_255330989";
  9. //for aliyun
  10. model.TemplateParam = JsonConvert.SerializeObject(new { username = user.UserName, code = passwordResetCode });
  11. //for tencent-cloud
  12. //model.TemplateParam = JsonConvert.SerializeObject(new string[] { user.UserName, passwordResetCode });
  13. var result = await smsService.SendSmsAsync(model);
  14. if (string.IsNullOrEmpty(result.BizId) && result.Code != "OK")
  15. {
  16. throw new UserFriendlyException("验证码发送失败,错误信息:" + result.Message);
  17. }
  18. }

创建接口

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

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

  1. [AbpAllowAnonymous]
  2. public async Task SendForgotPasswordCaptcha(ForgotPasswordProviderDto input)
  3. {
  4. var provider = input.Provider;
  5. var phoneNumberOrEmail = input.ProviderNumber;
  6. await forgotPasswordManager.SendForgotPasswordCaptchaAsync(provider, phoneNumberOrEmail);
  7. }
  8. [AbpAllowAnonymous]
  9. public async Task VerifyAndSendResetPasswordLink(SendResetPasswordLinkDto input)
  10. {
  11. var provider = input.Provider;
  12. var token = input.Token;
  13. await forgotPasswordManager.VerifyAndSendResetPasswordLinkAsync(token, provider);
  14. }

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

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

密码强制过期策略

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

  1. [Required]
  2. public DateTime LastPasswordModificationTime { get; set; }

改写接口

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

  1. public string PasswordResetCode { get; set; }

打开TokenAuthController,注入ResetPasswordManager服务对象

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

  1. [HttpPost]
  2. public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model)
  3. {
  4. var loginResult = await GetLoginResultAsync(
  5. model.UserNameOrEmailAddress,
  6. model.Password,
  7. GetTenancyNameOrNull()
  8. );
  9. ...
  10. //Password Expiration Check
  11. if (DateTime.Now - loginResult.User.LastPasswordModificationTime > TimeSpan.FromDays(90))
  12. {
  13. loginResult.User.SetNewPasswordResetCode();
  14. return new AuthenticateResultModel
  15. {
  16. PasswordResetCode = loginResult.User.PasswordResetCode,
  17. UserId = loginResult.User.Id,
  18. };
  19. }
  20. }

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



Vue网页端开发

重置密码页面

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

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

连接格式如下

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

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

  1. <template>
  2. <div id="app">
  3. <div class="title-container center">
  4. <h3 class="title">修改密码</h3>
  5. </div>
  6. <el-row>
  7. <el-form
  8. ref="loginForm"
  9. :model="input"
  10. class="login-form"
  11. autocomplete="on"
  12. label-position="left"
  13. >
  14. <el-form-item label="验证码">
  15. <el-input v-model="input.code" placeholder="请输入验证码" clearable />
  16. </el-form-item>
  17. <el-form-item label="新密码" prop="newPassword">
  18. <el-input
  19. v-model="input.newPassword"
  20. placeholder="请输入新密码"
  21. clearable
  22. show-password
  23. />
  24. </el-form-item>
  25. <el-form-item label="新密码(确认)" prop="newPassword2">
  26. <el-input
  27. v-model="input.newPassword2"
  28. placeholder="请再次输入新密码"
  29. clearable
  30. show-password
  31. />
  32. </el-form-item>
  33. <el-row type="flex" class="row-bg">
  34. <el-col :offset="6" :span="10">
  35. <el-button
  36. type="primary"
  37. style="width: 100%"
  38. @click.native.prevent="submit"
  39. >修改
  40. </el-button>
  41. </el-col>
  42. </el-row>
  43. </el-form>
  44. </el-row>
  45. </div>
  46. </template>

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

  1. created: async function () {
  2. var url = window.location.href;
  3. var reg = /[?&]([^?&#]+)=([^?&#]+)/g;
  4. var param = {};
  5. var ret = reg.exec(url);
  6. while (ret) {
  7. param[ret[1]] = ret[2];
  8. ret = reg.exec(url);
  9. }
  10. if ("code" in param) {
  11. this.input.code = param["code"];
  12. }
  13. if ("userId" in param) {
  14. this.input.userId = param["userId"];
  15. }
  16. },

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

  1. async submit() {
  2. if ((this.input.newPassword != this.input.newPassword2) == null) {
  3. this.$message.warning("两次输入的密码不一致!");
  4. return;
  5. }
  6. await request(
  7. `${this.host}${this.prefix}/User/ResetPasswordByCode`,
  8. "post",
  9. {
  10. userId: this.input.userId,
  11. newPassword: this.input.newPassword,
  12. resetCode: this.input.code,
  13. }
  14. )
  15. .catch((re) => {
  16. var res = re.response.data;
  17. this.errorMessage(res.error.message);
  18. })
  19. .then(async (res) => {
  20. var data = res.data.result;
  21. this.successMessage("密码修改成功!");
  22. window.location.href = "/reset-password-sample.html";
  23. })
  24. .finally(() => {
  25. setTimeout(() => {
  26. this.loading = false;
  27. }, 1.5 * 1000);
  28. });
  29. },

忘记密码控件

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

resetPasswordStage 是判定当前是哪个阶段的变量,

0表示正常用户名密码登录(初始状态),1表示输入手机号或邮箱验证身份,2表示通过验证即将发送重置密码的链接。

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

  1. <template v-else-if="resetPasswordStage == 1">
  2. <p>
  3. 请输入与要找回的账户关联的手机号或邮箱。我们将为你发送密码重置连接
  4. </p>
  5. <el-tabs tab-position="top" v-model="forgotPasswordProvider.provider">
  6. <el-tab-pane :lazy="true" label="通过手机号找回" name="Phone">
  7. <el-row>
  8. <el-col :span="24">
  9. <el-input
  10. v-model="forgotPasswordProvider.providerNumber"
  11. :placeholder="'请输入手机号'"
  12. tabindex="2"
  13. >
  14. <el-button
  15. slot="append"
  16. @click="sendResetPasswordLink"
  17. :disabled="forgotPasswordProvider.providerNumber == ''"
  18. >下一步</el-button
  19. >
  20. </el-input>
  21. </el-col>
  22. </el-row>
  23. </el-tab-pane>
  24. <el-tab-pane :lazy="true" label="通过邮箱找回" name="Email">
  25. <el-row>
  26. <el-col :span="24">
  27. <el-alert
  28. v-if="showResetRequireSuccess"
  29. title="密码重置连接已发送至登录用户对应的邮箱,请查收"
  30. type="info"
  31. >
  32. </el-alert>
  33. </el-col>
  34. <el-col :span="24">
  35. <p>建设中..</p>
  36. </el-col>
  37. </el-row>
  38. </el-tab-pane>
  39. </el-tabs>
  40. </template>

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

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

  1. async sendResetPasswordLink() {
  2. await request(
  3. `${this.host}${this.prefix}/User/SendForgotPasswordCaptcha`,
  4. "post",
  5. this.forgotPasswordProvider
  6. )
  7. .catch((re) => {
  8. var res = re.response.data;
  9. this.errorMessage(res.error.message);
  10. })
  11. .then(async (re) => {
  12. if (re) {
  13. this.successMessage("发送验证码成功");
  14. this.resetPasswordStage++;
  15. }
  16. });
  17. },
  18. async verifyAndSendResetPasswordLink() {
  19. await request(
  20. `${this.host}${this.prefix}/User/VerifyAndSendResetPasswordLink`,
  21. "post",
  22. {
  23. provider: this.forgotPasswordProvider.provider,
  24. token: this.captchaToken,
  25. }
  26. )
  27. .catch((re) => {
  28. var res = re.response.data;
  29. this.errorMessage(res.error.message);
  30. })
  31. .then(async (re) => {
  32. if (re) {
  33. this.successMessage("发送连接成功");
  34. this.resetPasswordStage = 0;
  35. }
  36. });
  37. },

密码过期提示

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

  1. <el-alert
  2. v-if="passwordResetCode != null"
  3. close-text="点此修改密码"
  4. title="密码已超过90天未修改,为了安全,请修改密码"
  5. type="info"
  6. @close="
  7. gotoUrl(
  8. '/reset-password-sample/reset.html?code=' +
  9. passwordResetCode +
  10. '&userId=' +
  11. userId
  12. )
  13. "
  14. >
  15. </el-alert>



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

项目地址

Github:matoapp-samples

用Abp实现找回密码和密码强制过期策略的更多相关文章

  1. 强制找回gitlab管理员密码

    强制找回gitlab管理员密码 最近使用gitlab的时候发现管理员密码忘记,现将找回密码的操作过程记录下来. 1.在gitlab登录窗口 如果密码忘记了登录不进入,可以先尝试点击登录框下方的Forg ...

  2. 三种找回 linux root密码

    找回 linux root密码的三种方法 第1种方法: 1.在系统进入单用户状态,直接用passwd root去更改2.用安装光盘引导系统,进行linux rescue状态,将原来/分区挂接上来,作法 ...

  3. 三种找回 linux root密码的方法

    找回 linux root密码的三种方法 第1种方法: 1.在系统进入单用户状态,直接用passwd root去更改2.用安装光盘引导系统,进行linux rescue状态,将原来/分区挂接上来,作法 ...

  4. Laravel实现找回密码及密码重置的例子

    https://mp.weixin.qq.com/s/PO5f5OJPt5FzUZr-7Xz8-g Laravel实现找回密码及密码重置功能在php实现与在这里实现会有什么区别呢,下面我们来看看Lar ...

  5. Ubuntu16---安装mysql5.7未提示输入密码,安装后修改mysql密码默认密码

    Ubuntu16安装mysql5.7未提示输入密码,安装后修改mysql密码默认密码 mysql默认密码为空 但是使用mysql -uroot -p 命令连接mysql时,报错 ERROR 1045 ...

  6. Postgres使用ALTER USER命令修改用户的密码、密码过期,锁定,解锁

    使用ALTER USER命令修改用户的密码.密码过期,锁定,解锁 (1)修改用户的口令,将用户的口令修改为新的密码 highgo=#create user test with password ‘te ...

  7. MYSQL命令练习及跳过数据库密码进行密码重新设置

        2.看当前所有数据库:show databases; 3.进入mysql数据库:use mysql; 4.查看mysql数据库中所有的表:show tables; 5.查看user表中的数据: ...

  8. MySQL密码复杂度与密码过期策略介绍

    前言: 年底了,你的数据库是不是该巡检了?一般巡检都会关心密码安全问题,比如密码复杂度设置,是否有定期修改等.特别是进行等保评测时,评测机构会要求具备密码安全策略.其实 MySQL 系统本身可以设置密 ...

  9. 如何修改oracle数据库用户密码过期策略

    转至:https://www.cnblogs.com/zhangshuaihui/p/11451590.html 1.   查询数据库用户何时过期 登陆数据库PL/SQL工具,输入以下sql语句: s ...

  10. Github强制找回管理员账号密码

    步骤: 1. 登录Github所在的服务器,切换用户为git:su git 2. 进入Github的Rails控制台:gitlab-rails console production 3. 查看超级管理 ...

随机推荐

  1. BubbleSort,冒泡排序,C++非递归和递归实现

    1 // g++ bubble_sort.cc -Wall -O3 && ./a.exe 2 3 4 #include <iostream> 5 #include < ...

  2. Windows10常用快捷键总结

    --Windows10常用快捷键总结 1. Window键: 打开或关闭|开始菜单 2. Win + A 打开操作中心 3. Win + D 显示桌面 4. Win + E 打开计算机文件管理器 5. ...

  3. Linux系列(8)-添加用户并设置密码

    #添加用户[root@iZm5ehnt0e8indgne1hibuZ ~]# useradd -m linsiyu #设置用户密码[root@iZm5ehnt0e8indgne1hibuZ ~]# p ...

  4. IDEA创建Spring Boot项目无法连接http://start.spring.io 解决方法

    1.修改代理 2. 搭建自己的SpringBoot initializer构建服务器 https://blog.csdn.net/KingBoyWorld/article/details/773732 ...

  5. vue中的普通函数与箭头函数以及this关键字

    普通函数 普通函数指的是用function定义的函数 var hello = function () { console.log("Hello, Fundebug!"); } 箭头 ...

  6. Scrapy框架报错:Ignoring non-200 response

    1.当爬取页面状态码是异常状态码,但response是正常的时候,正常情况Scrapy框架会判断状态码,如果不是正常状态码会停止后续操作 解决方案: 在meta"handle_httpsta ...

  7. java 通过反射以及MethodHandle执行泛型参数的静态方法

    开发过程中遇到一个不能直接调用泛型工具类的方法,因此需要通过反射来摆脱直接依赖. 被调用静态方法示例 public class test{ public static <T> T get( ...

  8. curl解决乱码

    mb_convert_encoding($str, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');

  9. 使用 Vue 3 时应避免的 10 个错误

    Vue 3已经稳定了相当长一段时间了.许多代码库都在生产环境中使用它,其他人最终都将不得不迁移到Vue 3.我现在有机会使用它并记录了我的错误,下面这些错误你可能想要避免. 使用Reactive声明原 ...

  10. 一篇文章带你快速入门学习RPA

    大纲: 什么是RPA? RPA的应用领域有哪些? RPA机器人技术一般用于什么行业? RPA的市场需求是什么? RPA项目的实施过程? RPA的未来趋势怎么样?   什么是RPA?   RPA 全称& ...