/account
下边的路径现在还剩下profile
和重置密码的没有写。我打算把展示用户信息和修改用户信息做在一个页面里。参考了这个页面的结构。
基础设计
由于上一次重构了模板继承关系,在/templates/account
下边创建profile.html
,直接采用基础模板如下:
<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org"
th:replace="~{/shared/standard::layout(~{::title}, ~{::style}, ~{::main}, ~{::script})}">
<head>
<title></title>
<style></style>
</head>
<body>
<main>
</main>
<script src=""></script>
</body>
</html>
模板样式
这个模板我采用了刚才页面的右侧部分来展示用户信息,将其排在页面的中央。然后把显示金额的地方换成了两个带图标的按钮,用来显示和隐藏两个表单,一个用来修改密码,一个用来修改邮箱。
之后使用了Collapse
组件来达到点击两个按钮的时候显示和隐藏表单的效果。
页面的效果如下:
点击按钮展开表单的效果如下:
URL
设计
我这里设计了一批URL
如下:
/account/profile
,用来展示用户资料/account/profile/password
,GET请求跳转到/account/profile
,POST请求用来修改密码/account/profile/email
,GET请求跳转到/account/profile
,POST请求用来修改电子邮件
这里感觉设计的比较丑陋,不过能够达成效果就行。
控制器与表单
这里我们打算复用UserRegistraion
这个VO
,不打算编写新的与用户交互的数据对象,因此要在控制器和表单上做些手脚。
/account/profile
控制器
这个地址用于返回用户页,由于必须登录才能够访问这个地址,因此控制器的核心功能就是取出用户的数据,然后渲染到页面上,如下:
@GetMapping("/profile")
public String profile(HttpServletRequest request, Model model) {
UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
UserRegistration registration = new UserRegistration(userData.getUsername(), userData.getEmail(), "", "");
model.addAttribute("userData", registration);
return "/account/profile";
}
核心逻辑就是从请求中取出已经验证的用户名称,然后从数据库中加载对应的UserData
,之后组装成UserRegistraion
对象,很显然,这次我们要复用这个VO
,不然还要为传入的部分字段再编写VO
,没有必要。
这里如果仔细看一下我们的VO
和BO
的关系就知道,二者之间没有任何联系,纯粹组装数据。不像BO
内部其实包含着对PO
的引用,因为二者的关系太密切。
将用户信息渲染到页面上很简单,然而针对两个表单,必须将一些信息以hidden
的input
框预先埋进去。
两个表单的设计
对于修改电子邮件的表单,设置成不需要输入密码,因此我们只需要一个input
框即可,埋入了一个用户名的input
,和两个没有用的password1
和password2
,以对应VO
的四个字段,表单如下:
<form action="/account/profile/email" th:action="@{/account/profile/email}" method="post"
th:object="${userData}">
<input type="hidden" th:value="*{username}" th:field="*{username}">
<input type="hidden" id="password1" name="password1" value="1">
<input type="hidden" id="password2" name="password2" value="1">
<div class="form-floating mb-3 justify-content-center">
<input type="email" class="form-control" id="email" name="email" placeholder="电子邮件" required>
<label>电子邮件</label>
<div th:if="${#fields.hasErrors('email')}" class="alert alert-danger" role="alert"
th:errors="*{email}"></div>
</div>
<div class="text-center mt-3 mb-2">
<button class="btn btn-primary">提交</button>
</div>
</form>
打算让用户看到表单中的的电子邮件框是空白,所以没有用th:field
绑定,而是手工设置上了id
和name
等属性。
对于修改密码的表单,我们的设计是输入原密码和新密码,这样两个密码正好能够复用VO
,由于VO
需要四个字段,因此埋了两个input
:
<form action="/account/profile/password" th:action="@{/account/profile/password}" method="post"
th:object="${userData}">
<input type="hidden" th:value="*{email}" th:field="*{email}">
<input type="hidden" th:value="*{username}" th:field="*{username}">
<div class="form-floating mb-3 justify-content-center">
<input type="password" class="form-control" th:field="*{password1}" placeholder="原密码" required>
<label>原密码</label>
<div th:if="${#fields.hasErrors('password1')}" class="alert alert-danger" role="alert"
th:errors="*{password1}"></div>
</div>
<div class="form-floating">
<input type="password" class="form-control" th:field="*{password2}" placeholder="新密码" required>
<label>新密码</label>
<div th:if="${#fields.hasErrors('password2')}" class="alert alert-danger" role="alert"
th:errors="*{password2}"></div>
</div>
<div class="text-center mt-3 mb-2">
<button class="btn btn-primary">提交</button>
</div>
</form>
上边两个表单其实埋入用户名和电子邮件有些多余,因为我们在逻辑中依旧是要先获取用户数据,不过留着也好,以后也许会加上一些验证。
修改电子邮件地址的控制器
这个控制器对应/account/profile/email
的POST请求,其核心逻辑是:
- 通过
request
获取BO
- 判断邮件地址是否通过验证,失败的话向视图传入
VO
和错误信息,返回/account/profile
视图 - 邮件地址通过验证,则将新的电子邮件地址设置到
BO
上 - 保存
BO
,由于设置了UserInfo
与User
的cascade = CascadeType.ALL
,所以保存User
的时候也会一并保存UserInfo
中更新的数据 - 返回成功信息,渲染
/account/profile
视图
这个控制器的逻辑比较简单,如下:
@PostMapping("/profile/email")
public String changeEmail(@ModelAttribute("userData") @Valid UserRegistration userRegistration, BindingResult rs, Model model, HttpServletRequest request) {
UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
if (rs.hasErrors()) {
model.addAttribute("emailError", "电子邮件格式不正确");
model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
return "/account/profile";
}
userData.setEmail(userRegistration.getEmail());
userService.saveUserData(userData);
model.addAttribute("emailSuccess", "成功修改电子邮件地址");
model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
return "/account/profile";
}
修改密码的控制器
这个控制器对应/account/profile/password
的POST请求,逻辑要比修改邮件复杂一些,如下:
- 同样是先通过
request
获取BO
- 判断密码是否通过验证,失败的话向视图传入
VO
和错误信息,返回/account/profile
视图 - 验证原密码是否正确,如果失败,设置错误信息,返回
/account/profile
视图 - 验证原密码成功,给
UserData
设置上passwordEncoder
加密后的密码,然后保存BO
- 非常关键的一步,让
session
失效,以让用户重新登录 - 返回提示用户重新登录的页面(此时
session
已经失效)
为此,在/templates/account/
下边创建一个passwordChanged.html
,其内容很简单,就一段提示:
<main class="form-signin">
<form action="/account/signout" th:action="@{/account/signout}" method="post">
<h3 class="h3 mb-3 fw-normal">您已修改密码,请重新登录</h3>
<a class="w-100 btn btn-lg btn-primary" th:href="@{/account/login}">登录</a>
</form>
</main>
然后来编写控制器:
@PostMapping("/profile/password")
public String changePassword(@ModelAttribute("userData") @Valid UserRegistration userRegistration, BindingResult rs, Model model, HttpServletRequest request) {
UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
if (rs.hasErrors()) {
model.addAttribute("passwordError", "请检查输入的密码");
model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
return "/account/profile";
}
//判断原密码是否正确,如果正确,给UserData设置上新密码,然后保存进数据库,之后要刷新session强制登录
if (passwordEncoder.matches(userRegistration.getPassword1(), userData.getPassword())) {
userData.setPassword(passwordEncoder.encode(userRegistration.getPassword2()));
userService.saveUserData(userData);
//取消会话
request.getSession().invalidate();
return "/account/passwordChanged";
} else {
model.addAttribute("passwordError", "原密码验证失败");
return "/account/profile";
}
}
两个控制器要注意的地方,就是无论出错或者成功完成修改,都要弄一个只包含用户名和密码的VO
传递给视图,以保证页面其他地方不出错。
这样就写好了用户修改资料的功能,不过我觉得我自己这里页面和URL设计的有些麻烦,尤其URL应该不用这么复杂,应该用一个页面让用户全部修改完毕,不过也算是折腾了一下各种用途。
接下来要编写用户重置密码的功能,这个我要仔细来考虑一下如何编写。