对于Web
应用,URL
对应资源,设计良好的URL
是非常关键的,而且在现代设计理念中,应当尽可能少的使用URL
传参,而是与资源地址进行交互,尤其REST
风格更是如此。
虽然我这个项目不搞前后端分离,不过还是尽量设计好URL
。
用户功能URL
设计
我想了一下,把用户功能的URL
设计如下:
/account/login
,用于GET
请求返回用户登录页面,已经编写完成。/account/auth
,Spring Security
配置的接受POST
验证的路径,已经编写完成。/account/logout
,准备编写一个页面,用于确认登出。/account/signout
,Spring Security
配置的用于登出的路径。/account/register
,自行编写,GET
请求用于获取用户注册页面,POST
请求用于接受用户注册表单,目前额外信息就一个:用户的邮箱。/account/profile
,修改用户信息和密码的界面,GET
请求返回表单,POST
请求用于接受用户的修改。/account/reset
,用户重置密码的界面,这个需要发一个邮件给用户,让其点击其中的路径,如何生成路径的业务逻辑,我需要再想一想,还涉及发送邮件的配置,都需要学习一下,可以等思考成熟了再编写。
编写登出功能
在开启CSRF
(默认开启)的情况下,可以配置Spring Security
用于登出的地址,而且这个地址需要接受一个POST
请求就可以取消登录状态,为此先来配置Spring Security
的登出地址:
配置Spring Security
的logout
功能
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/img/**", "/js/**", "/webfonts/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/account/login").loginProcessingUrl("/account/auth").permitAll()
.and()
.logout().logoutUrl("/account/signout").permitAll();
}
现在/account/signout
接受POST请求就可以取消登录状态。
按照刚才的设计,要为/account/logout
编写控制器和页面,其中用一个按钮来选择是否登出。
编写页面和控制器
登出的页面可以参考登录的页面,只需要保留一个按钮和说明文字即可,我们把这个页面叫做logout.html
。
<main class="form-signin">
<form action="/account/signout" th:action="@{/account/signout}" method="post">
<h1 class="h3 mb-3 fw-normal">确认登出</h1>
<button class="w-100 btn btn-lg btn-primary" type="submit">登出</button>
</form>
</main>
表单action
的地址需要变成/account/signout
,与Spring Security
的配置保持一致。
然后就是来编写返回这个页面的控制器,这个就比较简单了,无需做任何处理。而且这个页面在目前的配置下,不登录是无法访问的。仅仅只有/account/login
和/account/signout
是配置成了permitAll()
。
@RequestMapping("/logout")
public String logout() {
return "/account/logout";
}
编写好之后运行项目,先按照提示登录,然后输入/account/logout
地址,就可以进入登出页面,点击按钮即可登出。
修改在登录状态下访问登录页面的逻辑
一般在登录状态下访问登录页面,可以有几种选择,一是提示已经登录,让其选择返回或者登出,二是重定向到登出页面,三是重定向到其他页面,比如首页。
这里我选择重定向到首页。
这里的核心逻辑就是需要在控制器中判断当前请求是否已经通过身份验证,由于采用了Spring Security
的登录验证,其结果是符合Java Web
容器中的request.getPrinciple()
的规则,所以就可以用其来进行判断,修改account/login
路径的控制器如下:
@RequestMapping("/login")
public String login(@RequestParam(required = false, name = "error") String failed, HttpServletRequest request, Model model) {
// 如果用户已经登录,直接重定向到首页
if (request.getUserPrincipal() != null) {
return "redirect:/";
}
if (failed != null) {
model.addAttribute("failed", "用户名或密码错误,或用户未激活,请联系管理员");
}
return "/account/login";
}
这里采用了简单一点的方式,直接重定向到根目录。不过目前我们还没有编写根目录的控制器,因此会返回一个Spring
默认的错误页面,下一步我们就要自定义错误页面。
自定义错误页面
今天在想着这个事情,结果正好看到《Spring Boot
实战》这本书的62页,其中提到了存在Thymeleaf
的情况下,在/templates/
目录下放一个error.html
,Thymeleaf
就会去渲染这个页面。对比在/templates/error/
下放置状态码对应的页面文件,Thymeleaf
渲染的这个页面可以完整的引用样式,而状态码对应的文件由于渲染的层级不同,并不是使用Thymeleaf
,所以不是很方便。这里我就按照放置error.html
的方式。
编写这个页面的时候,目前我们的错误会有两种,一种是不具备权限=403
错误,当然其实我现在的功能也没有用到权限配置,还有一种就是404
错误。为此需要使用th:if
标签来进行判断,页面则依然可以采用之前与login.html
类似的居中的样式:
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="/shared/base::standardHead('错误')"></head>
<body>
<main class="container">
<h3 class="text-center mt-3 mb-3">出错啦</h3>
<ul class="list-group">
<li class="list-group-item">状态:[[${status}]]</li>
<li class="list-group-item">路径:[[${path}]]</li>
<li class="list-group-item">异常:[[${error}]]</li>
<li class="list-group-item">时间:[[${timestamp}]]</li>
<li class="list-group-item">类名:[[${exception}]]</li>
<li class="list-group-item">信息:[[${message}]]</li>
</ul>
<p th:if="${status} == '404'" class="text-center mt-2">访问的页面不存在</p>
<p th:if="${status} == '500'" class="text-center mt-2">服务器错误,请将错误信息<a href="mailto:liyiming@sinochem.com">发送给管理员</a></p>
<p th:if="${status} == '403'" class="text-center mt-2">不具备权限,请<a href="mailto:liyiming@sinochem.com">联系管理员</a></p>
<p class="text-center"><a class="btn btn-primary" href="/" th:href="@{/}">返回首页</a></p>
</main>
</body>
</html>
这里使用了Spring
传给页面的一系列属性,然后根据状态码来判断到底是哪一种错误,现在登录之后自动跳转到首页,就会看到这个错误页面。还有一个trace
属性是详细错误信息,这个没有必要展示给用户了。