【Ruoyi管理后台】用户登录强制修改密码

作为一个后台管理系统,用户的登录信息安全是很重要的,所以避免用户账号密码被破解泄露,一般都会采取强制用户在一段时间内需要重新修改更新一次密码,否则就不能正常登录系统。
本教程使用的若依前后端分离版本 v3.8.8

视频教程 https://bit.ly/4fUrwcL
教程markdown文档,关注 [大卷学长] 公众号回复 [ry1118]

一、后端项目调整

我们需要在用户表增加一个字段,用于标记用户最近一次修改密码的时间。

调整表结构

当前系统中的用户表并不支持记录用户的最后修改密码的时间,所以我们需要增加一个的字段来记录。

用户表 sys_user 加入 pwd_time 字段

每次用户更新密码时,都需要更新此字段为当前系统时间。

表结构调整

调整数据表映射文件 SysUserMapper.xml

resultMap 中增加 pwd_time 字段

mapper调整_01

查询语句中,增加 pwd_time 字段

mapper调整_02

重置密码语句中,增加 pwd_time 字段的更新,并设置为当前时间

mapper调整_03

调整实体对象 SysUser

ruoyi-common 项目 com.ruoyi.common.core.domain.entity.SysUser

添加 pwdTime 字段,注意要添加对应的 get/set 方法。

entity调整_01

调整登录接口

为什么要在登录接口这个地方调整,是因为后面的重置密码接口是需要登录后的 token 才能够调用成功,否则会提示登录已过期。

SysLoginService

ruoyi-framework 项目 com.ruoyi.framework.web.service.SysLoginService

新增加入判断用户是否已超过三个月没修改过密码的方法,这里具体是多长时间,可以自定义。

public boolean isPwdExpire(String username) {

    SysUser sysUser = userService.selectUserByUserName(username);
    Date pwdTime = sysUser.getPwdTime();

    LocalDate pwdDate = pwdTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    // 获取当前日期
    LocalDate currentDate = LocalDate.now();
    // 计算日期差异
    Period period = Period.between(pwdDate, currentDate);
    // 检查月份差异是否大于等于三个月
    if (period.toTotalMonths() >= 3) {
        return true;
    }

    return false;
}

SysLoginController

ruoyi-admin 项目 com.ruoyi.web.controller.system.SysLoginController

在 login 方法内,加入上面的方法判断,如果是密码过期了,就返回 res_code=1001,用于页面判断当前登录用户是否需要重置密码。同时出于安全的考虑,还使用了一些校验机制,生成一个sign标记,并设置在 redis 缓存里,用于重置密码时校验。

//加入 redis 缓存工具类
@Autowired
private RedisCache redisCache;

/**
 * 登录方法
 * 
 * @param loginBody 登录信息
 * @return 结果
 */
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
    AjaxResult ajax = AjaxResult.success();
    // 生成令牌
    String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
            loginBody.getUuid());
    //加入密码过期判断
    boolean pwdExpire = loginService.isPwdExpire(loginBody.getUsername());
    if (pwdExpire) {
        ajax.put("res_code", 1001);
        String signKey = CacheConstants.RESET_SIGN_KEY + loginBody.getUsername();
        String signCode = IdUtils.fastSimpleUUID();
        redisCache.setCacheObject(signKey, signCode, Constants.RESET_EXPIRATION, TimeUnit.MINUTES);
        ajax.put("reset_sign",signCode);
    }

    ajax.put(Constants.TOKEN, token);
    return ajax;
}

ruoyi-common 项目 com.ruoyi.common.constant.Constants 加入常量。

/**
 * 密码重置有效期(分钟)
 */
public static final Integer RESET_EXPIRATION = 5;

ruoyi-common 项目 com.ruoyi.common.constant.CacheConstants 加入常量。

/**
 * 密码重置sign redis key
 */
public static final String RESET_SIGN_KEY = "reset_sign:";

加入重置密码接口

在后台管理系统的个人中心有一个重置密码的地方,对应的也有一个重置密码的接口,但这是在用户已登录能正常进入后台的情况下操作的。

ruoyi-admin 项目 com.ruoyi.web.controller.system.SysProfileControllerupdatePwd 方法。

后台个人中心修改密码_01

而我们是需要在用户登录的时候,处理用户重置密码的流程,所以我们需要另外写一个接口来处理登录时的密码重置。

新增修改密码的请求实体对象 ResetBody

ruoyi-common 项目 com.ruoyi.common.core.domain.model

public class ResetBody {

    /**
     * 用户名
     */
    private String username;

    private String oldPassword;

    private String newPassword;

    private String confirmPassword;

    private String code;

    private String sign;

    private String uuid;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getOldPassword() {
        return oldPassword;
    }

    public void setOldPassword(String oldPassword) {
        this.oldPassword = oldPassword;
    }

    public String getNewPassword() {
        return newPassword;
    }

    public void setNewPassword(String newPassword) {
        this.newPassword = newPassword;
    }

    public String getConfirmPassword() {
        return confirmPassword;
    }

    public void setConfirmPassword(String confirmPassword) {
        this.confirmPassword = confirmPassword;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getSign() {
        return sign;
    }

    public void setSign(String sign) {
        this.sign = sign;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }
}

新增校验验证码的方法

ruoyi-framework 项目 com.ruoyi.framework.web.service.SysLoginService 添加新的 validateCaptcha 方法。

    /**
     * 校验验证码
     *
     * @param username 用户名
     * @param code 验证码
     * @param uuid 唯一标识
     * @param delete 是否删除缓存
     * @return 结果
     */
    public void validateCaptcha(String username, String code, String uuid, boolean delete)
    {
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);

        if (delete) {
            redisCache.deleteObject(verifyKey);
        }

        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
    }

重置密码接口

ruoyi-admin 项目 com.ruoyi.web.controller.system.SysProfileController 添加 resetPwd 接口。

//加入 SysLoginService
@Autowired
private SysLoginService sysLoginService;

//加入 redis 缓存工具类
@Autowired
private RedisCache redisCache;

/**
 * 重置密码
 */
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PostMapping("/resetPwd")
public AjaxResult resetPwd(@RequestBody ResetBody resetBody)
{

    String username = resetBody.getUsername();
    String sign = resetBody.getSign();

    String signKey = CacheConstants.RESET_SIGN_KEY + username;

    String cacheSign = redisCache.getCacheObject(signKey);
    if (StringUtils.isEmpty(cacheSign)) {
        return AjaxResult.error("链接已失效");
    }

    if (!cacheSign.equals(sign)) {
        return AjaxResult.error("sign有误");
    }

    String code = resetBody.getCode();
    String uuid = resetBody.getUuid();

    sysLoginService.validateCaptcha(username, code, uuid, false);

    SysUser sysUser = userService.selectUserByUserName(username);
    if (sysUser == null) {
        return AjaxResult.error("用户不存在");
    }

    String oldPassword = resetBody.getOldPassword();
    String password = sysUser.getPassword();

    if (!SecurityUtils.matchesPassword(oldPassword, password)) {
        return AjaxResult.error("修改密码失败,旧密码错误");
    }

    String newPassword = resetBody.getNewPassword();

    if (userService.resetUserPwd(username, SecurityUtils.encryptPassword(newPassword)) > 0)
    {
        LoginUser loginUser = getLoginUser();
        // 删除用户缓存记录
        tokenService.delLoginUser(loginUser.getToken());
        // 删除缓存
        redisCache.deleteObject(signKey);
        // 前端重定向到 login 页面
        return AjaxResult.success();
    }

    return AjaxResult.error("修改密码异常,请联系管理员");
}

方法中处理了一些校验,如上面说到的sign标记,还有页面上的图形验证码等,增加安全性。重置密码生成后,需要清除用户缓存记录,用于使用户必须重新登录。

二、前端项目调整

调整用户登录模型

文件 src/store/modules/user.js

这里需要调用 login 接口后的返回信息,带回到登录页面,用于后续的操作。

前端user模型_01

调整用户 API 接口

文件 src/api/system/user.js

加入后端的重置密码接口

// 用户密码重置
export function resetUserProfilePwd(data) {
  return request({
    url: '/system/user/profile/resetPwd',
    method: 'post',
    data: data
  })
}

重置密码页面

新增文件 src/views/reset.vue

重点在于 handleReset 函数,可参考注释说明。密码规则为 长度12-20位,必须包含数字、大小写字母、符号,可自定义调整。

<template>
  <div class="register">
    <el-form ref="resetForm" :model="resetForm" :rules="resetRules" class="register-form">
      <h3 class="title">修改密码</h3>

      <el-form-item label="旧密码" prop="oldPassword">
        <el-input v-model="resetForm.oldPassword" placeholder="请输入旧密码" type="password" show-password/>
      </el-form-item>
      <el-form-item label="新密码" prop="newPassword">
        <el-input v-model="resetForm.newPassword" placeholder="12位由数字、大小写字母、符号组成" type="password" show-password/>
      </el-form-item>
      <el-form-item label="确认密码" prop="confirmPassword">
        <el-input v-model="resetForm.confirmPassword" placeholder="请确认密码" type="password" show-password/>
      </el-form-item>

      <el-form-item prop="code" v-if="captchaOnOff">
        <el-input
          v-model="resetForm.code"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
        >
          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
        </el-input>
        <div class="register-code">
          <img :src="codeUrl" @click="getCode" class="register-code-img"/>
        </div>
      </el-form-item>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleReset"
        >
          <span v-if="!loading">确认修改</span>
        </el-button>
      </el-form-item>
    </el-form>
    <!--  底部  -->
    <div class="el-register-footer">
      <span>Copyright © 2018-2021 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>

<script>
import { getCodeImg } from "@/api/login";
import { resetUserProfilePwd } from "@/api/system/user";
import { setToken } from '@/utils/auth'

export default {
  name: "Reset",
  data() {
    const equalToPassword = (rule, value, callback) => {
      if (this.resetForm.newPassword !== value) {
        callback(new Error("两次输入的密码不一致"));
      } else {
        const reg = /^(?![A-z0-9]+)(?![A-z~!@#%^&*()_+]+)(?![0-9~!@#%^&*()_+]+)([A-z0-9~!@#%^&*()_+]{12,})/g
        if (!reg.test(value)) {
          callback(new Error("输入的密码必须包含数字、大小写字母、符号"));
        }
        callback();
      }
    };
    return {
      codeUrl: "",
      resetForm: {
        username: "",
        oldPassword: "",
        newPassword: "",
        confirmPassword: "",
        code: "",
        sign: "",
        uuid: ""
      },
      resetRules: {
        oldPassword: [
          { required: true, trigger: "blur", message: "旧密码不能为空" },
        ],
        newPassword: [
          { required: true, message: "新密码不能为空", trigger: "blur" },
          { min: 12, max: 20, message: "长度在 12 到 20 个字符", trigger: "blur" }
        ],
        confirmPassword: [
          { required: true, message: "确认密码不能为空", trigger: "blur" },
          { required: true, validator: equalToPassword, trigger: "blur" }
        ],
        code: [{ required: true, trigger: "change", message: "请输入验证码" }]
      },
      loading: false,
      captchaOnOff: true
    };
  },
  created() {
    this.getCode();
  },
  mounted() {
    // 先清除token,防止回退后能直接登录,从而绕过强制重置密码的逻辑
    setToken('');
    // 获取当前链接的参数
    const params = this.route.query;
    this.resetForm.sign = params.sign;
    this.resetForm.username = params.username;
    this.token = params.token;
  },
  methods: {
    getCode() {
      getCodeImg().then(res => {
        this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
        if (this.captchaOnOff) {
          this.codeUrl = "data:image/gif;base64," + res.img;
          this.resetForm.uuid = res.uuid;
        }
      });
    },
    handleReset() {
      this.refs.resetForm.validate(valid => {
        if (valid) {
          this.loading = true;
          // 获取并设置从登录页拿到的token,调用接口需要登录成功的token,否则会提示过期
          setToken(localStorage.getItem("reset_token"));
          resetUserProfilePwd(this.resetForm).then(res => {
            this.alert("<font color='red'>修改成功,请重新登录</font>", '系统提示', {
              dangerouslyUseHTMLString: true,
              type: 'success'
            }).then(() => {
              //删除登录成功设置的token
              localStorage.removeItem("reset_token");
              //清除token,强制登录
              setToken("")
              // 跳转到登录页
              this.router.push("/login");
            }).catch(() => {});
          }).catch(() => {
            //调用接口异常时,这里也需要清理token,防止返回后自动登录
            setToken("")
            this.loading = false;
            if (this.captchaOnOff) {
              this.getCode();
            }
          })
        }
      });
    }
  }
};
</script>

<style rel="stylesheet/scss" lang="scss">
.register {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}
.title {
  margin: 0px auto 30px auto;
  text-align: center;
  color: #707070;
}

.register-form {
  border-radius: 6px;
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;
  .el-input {
    height: 38px;
    input {
      height: 38px;
    }
  }
  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 2px;
  }
}
.register-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}
.register-code {
  width: 33%;
  height: 38px;
  float: right;
  img {
    cursor: pointer;
    vertical-align: middle;
  }
}
.el-register-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}
.register-code-img {
  height: 38px;
}
</style>

调整路由

文件 src/router/index.js

加入跳转到重置密码页面的路由配置。

  {
    path: '/reset',
    component: (resolve) => require(['@/views/reset'], resolve),
    hidden: true
  }

调整用户登录逻辑

文件 src/views/login.vue

调整 handleLogin 函数,加入判断是否需要重置密码的逻辑。

前端用户登录逻辑_01

代码如下:

this.store.dispatch("Login", this.loginForm).then((res) => {
        if (res.res_code && res.res_code === 1001) {// 判断到后端接口返回的重置密码标识码
          // 先设置token
          localStorage.setItem("reset_token", res.token);
          // 重定向到重置密码页,并带上校验参数
          this.redirect = '/reset?' + 'sign=' + res.reset_sign + '&username=' + this.loginForm.username;
        }
        this.router.push({ path: this.redirect || "/" }).catch(()=>{});
      }).catch(() => {
        this.loading = false;
        if (this.captchaOnOff) {
          this.getCode();
        }
      });

三、测试

先将普通账号 ry 的密码更新调整为三个月前。

测试_01

接着登录一下账号。

测试_02

页面进行了跳转,来到了重置密码页面。

测试_03

需要校验旧密码,并输入新的密码。

测试_04

修改成功,并提示重新登录。

测试_05

重新登录

测试_06

此时看到 密码更新时间 已经更新

测试_07

版权声明:
作者:lrbmike
链接:https://blog.liurb.org/2024/11/18/ruoyi_login_requires_password_change/
来源:大卷学长
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>