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

近期有个需求,就是需要调整Ruoyi管理后台:用户如果三个月(长时间)未修改过密码,需要在登录时强制修改密码,否则不能登录系统。

一、后端项目调整

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

1.调整表结构

1)用户表 sys_user 加入 pwd_time 字段

每次更新密码时,都需要更新为当前系统时间。

2)调整对应的xml映射文件 SysUserMapper.xml

3)调整对应的实体对象 SysUser

同时加入对应的 get/set 方法。

2.调整登录接口

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

1)SysLoginService

加入判断用户是否已超过三个月没修改过密码

java 复制代码
    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;
    }

2)SysLoginController

在 login 方法内,加入上面的方法判断

java 复制代码
    boolean pwdExpire = loginService.isPwdExpire(loginBody.getUsername());
    if (pwdExpire) {
        ajax.put("res_code", 1001);
        String signKey = Constants.RESET_SIGN_KEY + loginBody.getUsername();
        String signCode = IdUtils.fastSimpleUUID();
        redisCache.setCacheObject(signKey, signCode, Constants.RESET_EXPIRATION, TimeUnit.MINUTES);
        ajax.put("reset_sign",signCode);
    }

这里加入了返回码 res_code ,用于页面判断当前登录用户是否要做重置密码的操作。

同时出于安全的考虑,还使用了一些校验机制,生成一个sign标记,并设置在redis缓存里,用于重置密码时校验。

3.加入重置密码接口

原本在后台管理系统的个人中心是有一个重置密码的地方,对应的也有一个重置密码的接口,但这是在用户已登录能正常进入后台的情况下操作的,跟我们这里的需求不一样,所以我们需要另外写一个接口来处理登录时的密码重置。

1)SysProfileController

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

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

        String signKey = Constants.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标记,还有页面上的图形验证码等,增加安全性。

重置密码生成后,需要清除用户缓存记录,用于使用户必须重新登录。

2)ResetBody

接收重置密码的请求参数。

java 复制代码
public class ResetBody {

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

    private String oldPassword;

    private String newPassword;

    private String confirmPassword;

    private String code;

    private String sign;

    private String uuid;

    //getter / setter ...
    
}

二、前端项目调整

1.调整 src/store/modules/user.js

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

2.调整 src/api/system/user.js

加入重置密码的接口

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

3.加入重置密码页面 src/views/reset.vue

html 复制代码
<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(() => {
            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>

重点在于 handleReset 函数,可参考注释说明。

密码规则为 长度12-20位,必须包含数字、大小写字母、符号。

4.调整 src/router/index.js

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

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

5.调整 src/views/login.vue

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

javascript 复制代码
    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();
            }
          });

三、说在最后

这个需求的难点在于,怎么在登录页强制跳转到重置密码页,但是又要防止用户此时返回到 index 地址路径时,不能成功登录到后台,因为这时 token 已经设置了。不过这个 token 也不能随便清掉,因为后面重置密码的接口在调用的时候,就需要用到这个 token。

所以整个需求的难点就围绕着怎么去处理这个登录后的 token,最后选择的方案就是将 token 先保存在 localStorage,这样就可以在跳转到重置密码页的时候先清掉 token,防止用户绕过返回到 index 首页,同时在调用重置密码接口的时候,先从 localStorage 拿回来 token,设置后再调用接口。

相关推荐
萧瑟余晖5 小时前
Spring Boot 配置体系详解
spring boot
Wang's Blog6 小时前
Java框架 SpringCloud 快速入门: Eureka 服务注册
java·spring cloud·eureka
liangshanbo12156 小时前
请求拦截器相关面试题
java·前端·javascript
她说彩礼65万6 小时前
C语言 const的使用
java·服务器·c语言
开开心心就好6 小时前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
Wang's Blog6 小时前
Java框架 SpringCloud 快速入门: Eureka 注册中心的搭建
java·spring cloud·eureka
专业程序开发源6 小时前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
泡茶喝茶写代码6 小时前
A股量化数据工程:从 REST 接口到策略信号(第 7 篇):盈利能力因子:毛利率、净利率与 ROE
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
小蒜学长6 小时前
基于微信小程序的遇见咖啡店点餐系统设计与实现(代码+数据库+LW)
java·spring boot·后端·微信小程序·咖啡店点餐系统
lisanmengmeng6 小时前
NRPE 添加命令(三)
java·linux·服务器