1. 概述
本文将带你实现一个常见的功能:用户在注册并登录后,可以自行修改密码。
这个功能看似简单,但涉及前端交互、后端安全校验、权限控制和测试验证等多个环节。我们一步步来,避免踩坑。
2. 前端页面:修改密码表单
先看一个极简的前端页面,包含旧密码、新密码和确认密码三个输入框:
<html>
<body>
<div id="errormsg" style="display:none"></div>
<div>
<input id="oldpass" name="oldpassword" type="password" placeholder="旧密码" />
<input id="pass" name="password" type="password" placeholder="新密码" />
<input id="passConfirm" type="password" placeholder="确认新密码" />
<span id="error" style="display:none; color:red">密码不匹配</span>
<button type="submit" onclick="savePass()">修改密码</button>
</div>
<script src="jquery.min.js"></script>
<script type="text/javascript">
var serverContext = [[@{/}]];
function savePass(){
var pass = $("#pass").val();
var valid = pass == $("#passConfirm").val();
if(!valid) {
$("#error").show();
return;
}
$.post(serverContext + "user/updatePassword",
{password: pass, oldpassword: $("#oldpass").val()} ,
function(data){
window.location.href = serverContext +"/home.html?message="+data.message;
})
.fail(function(data) {
$("#errormsg").show().html(data.responseJSON.message);
});
}
</script>
</body>
</html>
✅ 关键点说明:
- 使用 jQuery 发起
POST
请求到/user/updatePassword
- 前端做了简单的密码一致性校验,避免无效请求
- 错误信息通过
#errormsg
展示,来自后端的 JSON 响应 serverContext
是 Thymeleaf 模板中的上下文路径,确保 URL 正确
⚠️ 注意:生产环境建议使用更健壮的前端框架(如 Vue/React)和表单验证库,但这不是本文重点。
3. 后端接口:更新用户密码
接下来是核心的后端逻辑,对应上面的 POST 请求:
@PostMapping("/user/updatePassword")
@PreAuthorize("hasRole('READ_PRIVILEGE')")
public GenericResponse changeUserPassword(Locale locale,
@RequestParam("password") String password,
@RequestParam("oldpassword") String oldPassword) {
User user = userService.findUserByEmail(
SecurityContextHolder.getContext().getAuthentication().getName());
if (!userService.checkIfValidOldPassword(user, oldPassword)) {
throw new InvalidOldPasswordException();
}
userService.changeUserPassword(user, password);
return new GenericResponse(messages.getMessage("message.updatePasswordSuc", null, locale));
}
✅ 核心要点解析:
- ✅
@PreAuthorize("hasRole('READ_PRIVILEGE')")
保证只有登录且具备对应权限的用户才能访问该接口,防止未授权调用 ❌ - ✅ 通过
SecurityContextHolder
获取当前登录用户的邮箱(即用户名) - ✅ 调用
checkIfValidOldPassword
验证旧密码是否正确(通常会用PasswordEncoder
匹配加密后的密码) - ✅ 密码正确后调用
changeUserPassword
更新为新密码(自动加密存储) - ✅ 成功返回本地化提示消息,失败抛出自定义异常(由全局异常处理器捕获返回 400)
⚠️ 踩坑提醒:
- 不要直接比较明文密码!必须使用
PasswordEncoder.matches(rawPassword, encodedPassword)
@RequestParam
参数名必须与前端一致,否则收不到值- 异常建议抛出而非手动返回错误码,便于统一处理
4. 接口测试:覆盖关键场景
光写代码不够,必须有自动化测试保底。以下是基于 TestNG + RestAssured 的 API 测试用例。
4.1 测试配置与初始化
@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = { ConfigTest.class, PersistenceJPAConfig.class },
loader = AnnotationConfigContextLoader.class)
public class ChangePasswordApiTest {
private final String URL_PREFIX = "http://localhost:8080/";
private final String URL = URL_PREFIX + "/user/updatePassword";
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
FormAuthConfig formConfig = new FormAuthConfig(
URL_PREFIX + "/login", "username", "password");
@BeforeEach
public void init() {
User user = userRepository.findByEmail("testuser@example.com");
if (user == null) {
user = new User();
user.setFirstName("Test");
user.setLastName("Test");
user.setPassword(passwordEncoder.encode("test"));
user.setEmail("testuser@example.com");
user.setEnabled(true);
userRepository.save(user);
} else {
user.setPassword(passwordEncoder.encode("test"));
userRepository.save(user);
}
}
}
📌 初始化逻辑说明:
- 使用
@BeforeEach
确保每次测试前数据干净 - 用户邮箱为
testuser@example.com
,初始密码为"test"
(已加密) FormAuthConfig
用于模拟表单登录认证
4.2 场景一:登录用户正常修改密码 ✅
@Test
public void givenLoggedInUser_whenChangingPassword_thenCorrect() {
RequestSpecification request = RestAssured.given().auth()
.form("testuser@example.com", "test", formConfig);
Map<String, String> params = new HashMap<>();
params.put("oldpassword", "test");
params.put("password", "newtest");
Response response = request.with().params(params).post(URL);
assertEquals(200, response.statusCode());
assertTrue(response.body().asString().contains("Password updated successfully"));
}
📌 验证点:
- 登录状态下提交正确旧密码
- 期望状态码 200,响应包含成功提示
4.3 场景二:旧密码错误 ❌
@Test
public void givenWrongOldPassword_whenChangingPassword_thenBadRequest() {
RequestSpecification request = RestAssured.given().auth()
.form("testuser@example.com", "test", formConfig);
Map<String, String> params = new HashMap<>();
params.put("oldpassword", "abc");
params.put("password", "newtest");
Response response = request.with().params(params).post(URL);
assertEquals(400, response.statusCode());
assertTrue(response.body().asString().contains("Invalid Old Password"));
}
📌 验证点:
- 提交错误的旧密码
- 期望返回 400,并提示“旧密码无效”
4.4 场景三:未登录用户尝试修改密码 ⚠️
@Test
public void givenNotAuthenticatedUser_whenChangingPassword_thenRedirect() {
Map<String, String> params = new HashMap<>();
params.put("oldpassword", "abc");
params.put("password", "xyz");
Response response = RestAssured.with().params(params).post(URL);
assertEquals(302, response.statusCode());
assertFalse(response.body().asString().contains("Password updated successfully"));
}
📌 验证点:
- 未携带认证信息直接调用接口
- 由于
@PreAuthorize
拦截,应跳转登录页(302 Redirect) - 响应体不应包含成功提示
✅ 总结测试策略:
场景 | 输入 | 预期结果 |
---|---|---|
正常流程 | 正确旧密码 | 200 + 成功提示 |
旧密码错误 | 错误旧密码 | 400 + 错误提示 |
未认证访问 | 无登录态 | 302 跳转 |
这样才算覆盖了主要边界情况。
5. 小结
本文实现了一个完整且安全的“修改密码”功能,涵盖:
- 前端表单与 JS 交互
- 后端权限控制与密码校验
- 多场景自动化测试
整个流程简单粗暴但实用,适合集成到现有 Spring Security 项目中。
📌 生产环境可扩展方向:
- 添加密码强度校验(正则/策略模式)
- 记录密码修改日志
- 增加二次验证(如邮箱验证码)
- 支持密码历史防止重复使用
完整代码示例可参考 GitHub 项目:https://github.com/Baeldung/spring-security-registration
这是一个基于 Eclipse 的 Spring 项目,导入即可运行。