feat(admin): 修改密码功能(PBKDF2原密码校验 + 新密码≥8位合规 + 页面 + 导航入口)

- AdminUserService.changePassword: verify旧密码 + 新密码≥8位且≠原密码 + hash更新
- AdminController: GET/POST /admin/password(当前登录用户,Sa-Token loginId)
- password.ftl: 表单 + 前端JS预校验(长度/一致/新旧不同)
- _macros.ftl: 侧边栏footer加修改密码入口

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
连龙刚
2026-07-09 14:00:43 +08:00
co-authored by Claude
parent f401e8d065
commit 707f0b4824
5 changed files with 110 additions and 0 deletions
@@ -116,6 +116,39 @@ public class AdminController {
ctx.redirect(basePath + "/admin/login");
}
// ==================== 修改密码 ====================
@Get
@Mapping("/password")
public Object passwordPage(@Param(defaultValue = "") String msg) {
ModelAndView mv = view("password.ftl", "修改密码", "password");
mv.put("msg", msg);
return mv;
}
@Post
@Mapping("/password")
public void changePassword(@Param(defaultValue = "") String oldPassword,
@Param(defaultValue = "") String newPassword,
@Param(defaultValue = "") String confirmPassword,
Context ctx) throws Throwable {
// 两次新密码一致性前端已校验,后端兜底
if (!newPassword.equals(confirmPassword)) {
ctx.redirect(basePath + "/admin/password?msg=pwdconfirm");
return;
}
try {
// 当前登录用户IDSa-Token 登录时存的是 AdminUser.id
adminUserService.changePassword(StpUtil.getLoginIdAsLong(), oldPassword, newPassword);
log.info("管理员密码已修改 loginId={}", StpUtil.getLoginIdAsLong());
ctx.redirect(basePath + "/admin/password?msg=pwddone");
} catch (Exception e) {
// 原密码错误 / 新密码不合规统一提示,细节见日志
log.warn("修改密码失败 : {}", e.getMessage());
ctx.redirect(basePath + "/admin/password?msg=pwdfail");
}
}
// ==================== 首页(仪表盘) ====================
@Get
@@ -22,4 +22,16 @@ public interface AdminUserService {
* 应用启动时确保存在至少一个管理员(表空则按 app.yml imutil.admin 初始化)
*/
void ensureDefaultAdmin();
/**
* 修改当前登录用户密码
* <p>
* 校验原密码(PBKDF2 verify)+ 新密码合规(≥ 8 位且与原密码不同)后写新 hash。
*
* @param userId 用户ID
* @param oldPassword 原密码(明文)
* @param newPassword 新密码(明文,需 ≥ 8 位且与原密码不同)
* @throws com.imutil.common.BizException 原密码错误 / 新密码不合规 / 用户不存在
*/
void changePassword(Long userId, String oldPassword, String newPassword);
}
@@ -1,6 +1,7 @@
package com.imutil.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.imutil.common.BizException;
import com.imutil.common.PasswordUtil;
import com.imutil.entity.AdminUser;
import com.imutil.mapper.AdminUserMapper;
@@ -74,4 +75,26 @@ public class AdminUserServiceImpl implements AdminUserService {
adminUserMapper.insert(u);
log.info("已初始化默认管理员账号: {}(请尽快修改默认密码)", defaultUsername);
}
@Override
public void changePassword(Long userId, String oldPassword, String newPassword) {
AdminUser u = adminUserMapper.selectById(userId);
if (u == null) {
throw new BizException("用户不存在");
}
// 原密码校验(PBKDF2 常量时间比对)
if (!PasswordUtil.verify(oldPassword, u.getPasswordHash())) {
throw new BizException("原密码错误");
}
// 新密码合规:≥ 8 位且与原密码不同(防改成又一个弱密码)
if (newPassword == null || newPassword.length() < 8) {
throw new BizException("新密码至少 8 位");
}
if (newPassword.equals(oldPassword)) {
throw new BizException("新密码不能与原密码相同");
}
u.setPasswordHash(PasswordUtil.hash(newPassword));
adminUserMapper.updateById(u);
log.info("管理员密码已修改 userId={} username={}", userId, u.getUsername());
}
}
+1
View File
@@ -70,6 +70,7 @@
<a href="${basePath}/admin/migrate" class="nav-item <#if active=='migrate'>active</#if>">数据迁移</a>
</nav>
<div class="sidebar-footer">
<a href="${basePath}/admin/password" class="nav-item" style="border-left:none">修改密码</a>
<a href="${basePath}/admin/logout" class="logout-link">退出登录</a>
</div>
</aside>
+41
View File
@@ -0,0 +1,41 @@
<#--
修改密码页
校验:原密码正确 + 新密码 ≥ 8 位 + 新旧不同 + 两次新密码一致(前端 JS + 后端 AdminUserServiceImpl 兜底)
-->
<#import "_macros.ftl" as m>
<@m.layout active="password" title="修改密码">
<#if msg?has_content>
<div class="alert <#if msg!='pwddone'>alert-warn</#if>">
<#if msg=='pwddone'>✅ 密码修改成功,下次登录请使用新密码
<#elseif msg=='pwdfail'>❌ 修改失败:原密码错误,或新密码不合规(至少 8 位且与原密码不同)
<#elseif msg=='pwdconfirm'>❌ 两次输入的新密码不一致
</#if>
</div>
</#if>
<div style="max-width:420px;background:#fff;border-radius:8px;padding:24px 28px;box-shadow:0 1px 6px rgba(0,0,0,.07)">
<p class="muted" style="margin-bottom:16px">当前默认密码为弱密码(admin123),建议尽快修改为含字母+数字的强密码。</p>
<form method="post" action="${basePath}/admin/password" id="pwdForm">
<div class="modal-body">
<div class="form-group"><label>原密码</label><input type="password" name="oldPassword" required autocomplete="current-password"></div>
<div class="form-group"><label>新密码(至少 8 位,建议字母+数字)</label><input type="password" name="newPassword" id="newPwd" required autocomplete="new-password"></div>
<div class="form-group"><label>确认新密码</label><input type="password" name="confirmPassword" id="confirmPwd" required autocomplete="new-password"></div>
</div>
<div style="margin-top:8px;text-align:right">
<button type="submit" class="btn btn-primary">确认修改</button>
</div>
</form>
</div>
<script>
// 前端预校验:长度 ≥ 8 + 两次一致 + 新旧不同(后端服务层强校验兜底)
document.getElementById('pwdForm').addEventListener('submit', function (e) {
var np = document.getElementById('newPwd').value;
var cp = document.getElementById('confirmPwd').value;
var op = document.getElementsByName('oldPassword')[0].value;
if (np.length < 8) { e.preventDefault(); alert('新密码至少 8 位'); return; }
if (np !== cp) { e.preventDefault(); alert('两次输入的新密码不一致'); return; }
if (np === op) { e.preventDefault(); alert('新密码不能与原密码相同'); return; }
});
</script>
</@m.layout>