『博客开发日记-后台』之重置用户密码接口的实现

本文最后更新于 2026年5月20日 晚上

重置用户密码接口的实现


重置用户密码接口的需求

先检查用户是否存在

管理员密码只能由管理员本人重置

重置密码并加密入库

如果用户有邮箱,则发送重置密码通知邮件给用户


代码实现

AdminUserController

1
2
3
4
5
6
7
8
9
@PutMapping("/{id}/password/reset")
@PreAuthorize("@ps.hasPermission('sys:user:reset-password')")
@SystemLog(businessName = "重置用户密码")
@ApiOperation(value = "重置用户密码接口", notes = "重置用户密码", response = String.class)
@ApiImplicitParam(name = "id", value = "用户ID", dataType = "long", paramType = "path", required = true)
public ResponseResult updateResetUserPassword(@PathVariable Long id, String password)
{
return adminUserService.updateResetUserPassword(id, password);
}

由于该接口是直接把参数放到 URL 查询串里的,没有请求体

所以要给这个接口放行

要优化一下 ContentTypeValidationFilter 让他能支持 /{id}/password/reset 这种形式的接口url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
* Content-Type验证过滤器
* 防止Content-Type伪造攻击
*
* @author mengze
*/
@Slf4j
@Component
public class ContentTypeValidationFilter extends OncePerRequestFilter
{

/**
* 需要验证Content-Type的请求方法
*/
private static final List<String> METHODS_TO_VALIDATE = Arrays.asList("POST", "PUT", "PATCH");

/**
* 允许的Content-Type列表
*/
private static final List<String> ALLOWED_CONTENT_TYPES = Arrays.asList(
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
);

/**
* 白名单路径(不需要验证Content-Type)
*/
private static final List<String> WHITELIST_PATHS = Arrays.asList(
"/users/*/password/reset", // 重置密码接口
"/auth/refreshToken", // 刷新token接口
"/comment/like", // 点赞接口
"/auth/logout", // 退出登录
"/upload", // 文件上传接口
"/swagger", // Swagger文档
"/v2/api-docs", // Swagger API文档
"/v3/api-docs" // Swagger API文档
);

private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String method = request.getMethod();
String path = request.getRequestURI();

// 只验证POST、PUT、PATCH请求
if (!METHODS_TO_VALIDATE.contains(method)) {
filterChain.doFilter(request, response);
return;
}

// 检查是否在白名单中
if (isWhitelistPath(path)) {
filterChain.doFilter(request, response);
return;
}

// 获取Content-Type
String contentType = request.getContentType();

// Content-Type不能为空
if (contentType == null || contentType.isEmpty()) {
log.warn("请求缺少Content-Type header, path: {}, method: {}", path, method);
sendErrorResponse(response, "请求必须包含Content-Type");
return;
}

// 验证Content-Type是否合法(忽略charset等参数)
String baseContentType = contentType.split(";")[0].trim().toLowerCase();
boolean isValid = ALLOWED_CONTENT_TYPES.stream()
.anyMatch(allowed -> baseContentType.startsWith(allowed));

if (!isValid) {
log.warn("不支持的Content-Type: {}, path: {}, method: {}", contentType, path, method);
sendErrorResponse(response, "不支持的Content-Type: " + baseContentType);
return;
}

// 验证通过,继续处理
filterChain.doFilter(request, response);
}

/**
* 检查路径是否在白名单中
*/
private boolean isWhitelistPath(String path) {
return WHITELIST_PATHS.stream().anyMatch(pattern -> PATH_MATCHER.match(pattern, path));
}

/**
* 发送错误响应
*/
private void sendErrorResponse(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");

ResponseResult<?> result = ResponseResult.errorResult(
AppHttpCodeEnum.SYSTEM_ERROR.getCode(),
message
);

response.getWriter().write(JSON.toJSONString(result));
}
}


优化 SecurityUtils 里对管理员省份的判断

1
2
3
4
5
6
7
8
9
10
/**
* 判断当前用户是否为管理员
*/
public static boolean isAdmin() {
LoginUser loginUser = getLoginUser();
if (loginUser == null || loginUser.getSysUser() == null || loginUser.getSysUser().getId() == null) {
return false;
}
return loginUser.getSysUser().getId().equals(1L);
}

在 EmailServiceImpl 编写发送重置密码邮箱的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//发送用户密码重置通知邮件
@Async("asyncExecutor")
public void sendResetPasswordNotification(String toEmail, String nickname, String username, String newPassword)
{
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom(from);
helper.setTo(toEmail);
helper.setSubject("【云梦泽的个人博客】您的登录密码已重置");

String htmlContent = buildResetPasswordNotificationHtml(nickname, username, newPassword);
helper.setText(htmlContent, true);

mailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}

//用户密码重置通知邮件
private String buildResetPasswordNotificationHtml(String nickname, String username, String newPassword)
{
return "<!DOCTYPE html>" +
"<html>" +
"<head>" +
" <meta charset=\"UTF-8\">" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" +
"</head>" +
"<body style=\"margin: 0; padding: 0; background: linear-gradient(135deg, #a5dff9 0%, #008c9e 100%); font-family: 'Segoe UI', Arial, sans-serif;\">" +
" <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding: 40px 20px;\">" +
" <tr>" +
" <td align=\"center\">" +
" <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f5fffa; border-radius: 12px; box-shadow: 0 12px 15px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); overflow: hidden;\">" +
" <tr>" +
" <td style=\"background: rgb(125, 182, 191); padding: 40px 30px; text-align: center;\">" +
" <h1 style=\"color: #2c3e50; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: 1px;\">云梦泽的个人博客</h1>" +
" <p style=\"color: #546e7a; margin: 10px 0 0 0; font-size: 14px;\">密码重置通知</p>" +
" </td>" +
" </tr>" +
" <tr>" +
" <td style=\"padding: 50px 40px;\">" +
" <h2 style=\"color: #3c4858; margin: 0 0 20px 0; font-size: 22px; font-weight: 600;\">您好," + (StringUtils.hasText(nickname) ? nickname : username) + "!</h2>" +
" <p style=\"color: #718096; line-height: 1.8; margin: 0 0 30px 0; font-size: 15px;\">" +
" 您的登录密码已被管理员重置,请尽快使用下方新密码进行登录" +
" </p>" +
" <div style=\"background: linear-gradient(135deg, #f8f9fa 0%, #e8f4f8 100%); border-left: 4px solid #30a9de; border-radius: 8px; padding: 25px; margin: 30px 0; box-shadow: 0 2px 8px rgba(48,169,222,0.1);\">" +
" <p style=\"color: #3c4858; line-height: 1.8; margin: 0 0 15px 0; font-size: 15px; font-weight: 600;\">" +
" ( ⩌⤚⩌) 健忘的家伙,密码已经帮你重置好了" +
" </p>" +
" <p style=\"color: #718096; line-height: 1.8; margin: 0 0 12px 0; font-size: 14px;\">" +
" 您的账号(" + username + ")新密码为:" +
" </p>" +
" <div style=\"background-color: #ffffff; border-radius: 8px; padding: 16px 20px; text-align: center; border: 1px dashed #30a9de;\">" +
" <span style=\"font-size: 22px; font-weight: 700; letter-spacing: 2px; color: #30a9de; font-family: 'Courier New', monospace;\">" + newPassword + "</span>" +
" </div>" +
" </div>" +
" <div style=\"background-color: #fff8e1; border-left: 4px solid #ffc107; border-radius: 8px; padding: 20px; margin: 25px 0;\">" +
" <p style=\"color: #3c4858; line-height: 1.8; margin: 0; font-size: 14px;\">" +
" 请及时使用新密码登录,并在登录后立即修改为您自己的安全密码。" +
" </p>" +
" </div>" +
" </td>" +
" </tr>" +
" <tr>" +
" <td style=\"background: linear-gradient(135deg, #f8f9fa 0%, #eaecef 100%); padding: 30px; text-align: center; border-top: 1px solid #eaecef;\">" +
" <p style=\"color: #718096; margin: 0 0 10px 0; font-size: 13px;\">此邮件由系统自动发送,请勿直接回复</p>" +
" <p style=\"color: #a7a9ad; margin: 0; font-size: 12px;\">© " + LocalDate.now().getYear() + " 云梦泽的个人博客 · All rights reserved</p>" +
" </td>" +
" </tr>" +
" </table>" +
" </td>" +
" </tr>" +
" </table>" +
"</body>" +
"</html>";
}

最后是在 AdminUserServiceImpl 实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//重置用户密码
@Override
public ResponseResult updateResetUserPassword(Long id, String password)
{
//检查用户是否存在
SysUser user = adminUserService.getById(id);
if (user == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "该用户不存在!");
}

//管理员密码只能由管理员本人重置
if (Objects.equals(user.getId(), 1L) && !SecurityUtils.isAdmin()) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "管理员密码只能由管理员本人重置!");
}

//重置密码并加密入库
user.setPassword(new BCryptPasswordEncoder().encode(password));
boolean update = adminUserService.updateById(user);
if (!update) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "重置密码失败!");
}

//如果用户有邮箱,则发送重置密码通知邮件给用户
if (StringUtils.hasText(user.getEmail())) {
emailService.sendResetPasswordNotification(
user.getEmail(),
user.getNickname(),
user.getUsername(),
password
);
}

return ResponseResult.okResult();
}


在 AdminUserServiceImpl 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 //删除用户
@Override
@Transactional
public ResponseResult deleteUser(Long[] ids)
{
//判断用户是否存在
if (ids == null || ids.length == 0) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "请选择要删除的用户");
}

//不允许删除系统管理员用户
List<Long> userIdList = Arrays.asList(ids);
List<SysUser> usersToDelete = adminUserService.listByIds(userIdList);
List<Long> adminUserIds = usersToDelete.stream()
.map(SysUser::getId)
.filter(id -> Objects.equals(id, 1L))
.toList();
if (!adminUserIds.isEmpty()) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "系统管理员用户不能删除!");
}

//获取用户id列表
if (usersToDelete.size() != ids.length)
{
//找出不存在的 用户id
Set<Long> existIds = usersToDelete.stream()
.map(SysUser::getId)
.collect(Collectors.toSet());

List<Long> notExistIds = userIdList.stream()
.filter(id -> !existIds.contains(id))
.toList();

return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "删除失败!!原因:以下用户 id 不存在:" + notExistIds);
}

boolean removed = adminUserService.removeByIds(userIdList);
if (!removed) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "删除用户失败");
}

//执行删除
adminUserService.removeByIds(userIdList);

return ResponseResult.okResult();
}




PS:该系列只做为作者学习开发项目做的笔记用

不一定符合读者来学习,仅供参考


预告

后续会记录博客的开发过程

每次学习会做一份笔记来进行发表

“一花一世界,一叶一菩提”


版权所有 © 2026 云梦泽
欢迎访问我的个人网站:


『博客开发日记-后台』之重置用户密码接口的实现
http://example.com/2026/05/20/『博客开发日记-后台』之重置用户密码接口的实现/
作者
云梦泽
发布于
2026年5月20日
更新于
2026年5月20日
许可协议