用户长期未修改密码可能存在密码安全问题,项目要求提供一个配置使用户一定时间间隔就修改密码
后端登录时判断是否达到最大间隔时间,达到则修改用户的constraint字段(方便前端判断);
登录验证后,判断是否需要修改密码,需要修改则阻止跳转、弹出修改框;
限制:新密码不能与旧密码一致;
修改完成后正常跳转到主页面;
数据库更新密码、维护constraint字段。
src\main\java\com\ruoyi\framework\web\service\UserDetailsServiceImpl.java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
// 新增自动装配
@Autowired
private SysConfigMapper sysConfigMapper;
@Autowired
private SysUserMapper userMapper;
// 代码修改
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
}
// 用户信息没有密码最后修改日期,则直接强制修改密码
if (user.getPassTime() == null) {
user.setConstraint(true);
} else {
// 有最后修改日期则再进行判断
// 获取最大密码修改时间间隔(毫秒值)
// 若依已经写好的方法,直接拿来用就行
SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("sys.user.pass.interval");
Long interval = Long.parseLong(sysConfig.getConfigValue()) * 24 * 60 * 60 * 1000;
// 获取用户密码修改间隔
Long userInterval = System.currentTimeMillis() - user.getPassTime().getTime();
// 如果用户间隔超过间隔上限则要求强制修改密码
if (userInterval >= interval) {
user.setConstraint(true);
}
}
// 保存到数据库
// 若依已经写好的用户信息更新
userMapper.updateUser(user);
return createLoginUser(user);
}
src\main\java\com\ruoyi\web\controller\system\SysProfileController.java
// 修改前源码
// Javabean记得新增constraint属性和get、set
// userMapper.xml记得修改一下返回字段
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(String oldPassword, String newPassword)
{
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return AjaxResult.error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password))
{
return AjaxResult.error("新密码不能与旧密码相同");
}
if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0)
{
// 更新缓存用户密码
loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
tokenService.setLoginUser(loginUser);
return AjaxResult.success();
}
return AjaxResult.error("修改密码异常,请联系管理员");
}
// 修改后代码
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, Object> user) {
// 获取constraint
boolean constraint = (boolean) user.get("constraint");
// 防止传参异常或维护时出现问题
constraint = constraint == true ? constraint : false;
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
// 获取旧密码(为了不影响若依中原有的密码修改)
String oldPassword = (String) user.get("oldPassword");
// 获取新密码
String newPassword = (String) user.get("newPassword");
// 强制修改密码时不用判断旧密码
if (!constraint && !SecurityUtils.matchesPassword(oldPassword, password)) {
System.out.println("旧密码");
return AjaxResult.error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password)) {
System.out.println("新密码");
return AjaxResult.error("新密码不能与旧密码相同");
}
if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) {
// 更新缓存用户密码
loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
tokenService.setLoginUser(loginUser);
return AjaxResult.success();
}
return AjaxResult.error("修改密码异常,请联系管理员");
}
逻辑改变不大:
新增判断——强制修改时不比判断旧密码(传空就行);
接收类型:
这是由于开发过程中出现了一些问题、bug,排查过程中将入参类型修改了一下;
新增接收参数:constraint(true:强制修改密码,false:不强制)
ruoyi-ui\src\views\login.vue
// 新增弹出框标签
<el-dialog title="您的密码已长时间未修改,请修改密码" :visible.sync="loginInfo.constraint">
<el-form>
<el-form-item label="新密码">
<el-input v-model="newPassword" autocomplete="off">el-input>
el-form-item>
el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="updatePwd">确 定el-button>
div>
el-dialog>
// 导入方法
import { updateUserPwd } from "@/api/system/user";
//新增对象、变量
loginInfo:{
constraint: false
},
newPassword:'',
// 修改登录方法
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), {
expires: 30,
});
Cookies.set("rememberMe", this.loginForm.rememberMe, {
expires: 30,
});
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
this.$store
.dispatch("Login", this.loginForm)
.then((res) => {
getInfo().then(loginInfo => {
this.loginInfo = loginInfo.user
// 如果返回的constraint为false则正常跳转到主页面
if(!this.loginInfo.constraint) {
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
} else {
// 否则设置constraint值为true,使页面不能跳转到主页面
localStorage.setItem('constraint', true)
}
}).catch(() => {})
// const md5passwod = md5(this.loginForm.password);
// console.log("md5passwod" + md5passwod);
//用于后续解锁
// localStorage.setItem("username", this.loginForm.username);
})
.catch(() => {
this.loading = false;
if (this.captchaOnOff) {
this.getCode();
}
});
}
});
},
// 新增修改方法
updatePwd(){
updateUserPwd("", this.newPassword, true).then(response => {
// 修改完成将constraint改为false
localStorage.setItem('constraint', false)
this.$modal.msgSuccess("修改成功");
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
})
.catch((e) => {
console.info(e)
});
},
ruoyi-ui\src\api\system\user.js
// 新增constraint属性
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword, constraint) {
const data = {
oldPassword,
newPassword,
constraint
}
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
data: data
})
}
ruoyi-ui\src\permission.js
修改前
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
// 修改后
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
// localStorage.setItem('constraint',false)
if (to.path === '/login') {
// 新增判断——当前位置未登录页面时:如果constraint值为true
if(localStorage.getItem("constraint") == 'true') {
// 去掉token
setToken("")
}
// 不为true时不变
next({ path: '/' })
NProgress.done()
ruoyi-ui\src\views\system\user\profile\resetPwd.vue
submit() {
this.$refs["form"].validate(valid => {
if (valid) {
// 修改updateUserPwd方法的入参
// constraint值为true时不校验旧密码,传入false校验,不影响功能
updateUserPwd(this.user.oldPassword, this.user.newPassword, false).then(response => {
this.$modal.msgSuccess("修改成功");
});
}
});
},
这样一来我做的强制修改密码就不强制了,那怎么忍得了,这还是我一时兴起点了一下刷新试了试,结果果然直接跳过强制修改了
但是这样处理的时候就出现了一个问题(困扰了大半个小时,改了不少地方——尤其是后端的接收类型我都改成Map了……)
没错!修改密码这里需要token的校验,我把token清空了就无法正常调用了
src\main\java\com\ruoyi\web\controller\system\SysProfileController.java
// 新增代码
// 获取密码强度
SysConfig sysConfig = configMapper.checkConfigKeyUnique("sys.user.pass.strength");
String strength = sysConfig.getConfigValue();
Pattern p;
boolean strengthEnough = false;
switch (strength) {
// 强度3:必须含有特殊字符
case "3":
// 非数字、字母
p = Pattern.compile("[A-Za-z0-9]");
for (int i = 0; i < newPassword.length(); i++) {
if (!p.matcher(newPassword.substring(i, i + 1)).find()) {
strengthEnough = true;
break;
}
}
if (!strengthEnough) {
return AjaxResult.error("新密码必须含特殊字符");
}
// 强度2:必须含有大写和小写
case "2":
strengthEnough = false;
// 含大写字母
p = Pattern.compile("^[A-Z]+$");
for (int i = 0; i < newPassword.length(); i++) {
if (p.matcher(newPassword.substring(i, i + 1)).find()) {
// 含小写字母
p = Pattern.compile("^[a-z]+$");
for (int j = 0; j < newPassword.length(); j++) {
if (p.matcher(newPassword.substring(j, j + 1)).find()) {
strengthEnough = true;
break;
}
}
// 通过直接结束循环
if (strengthEnough) {
break;
}
}
}
if (!strengthEnough) {
return AjaxResult.error("新密码必须含大写和小写");
}
// 强度1:密码至少6位
case "1":
if (newPassword.length() < 6) {
return AjaxResult.error("新密码至少6位");
}
}