客户管理系统需要考虑验证⽤户的注册邮箱是否正确,使⽤
Filter
来判断⽤户的登录状态是否已经启⽤,以及在
项⽬中缓存的使⽤,如何使⽤
Thymeleaf
的最新语法判断表达式对⻚⾯布局,最后讲解使⽤
Docker
部署客户管
理系统。
邮箱验证
我们希望⽤户注册的邮箱信息是正确的,因此会引⼊邮件验证功能。注册成功后会给⽤户发送⼀封邮件,邮件中
会有⼀个关于⽤户的唯⼀链接,当单击此链接时更新⽤户状态,表明此邮箱即为⽤户真正使⽤的邮箱。
⾸先需要定义⼀个邮件模板,每次⽤户注册成功后调⽤模板进⾏发送。
邮件模板
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>邮件模板</title>
</head>
<body>
您好,感谢您的注册,请您尽快对注册邮件进⾏验证,请点击下⽅链接完成,感谢您的⽀持!<br/>
<a href="#" th:href="@{http://localhost:8080/verified/{id}(id=${id}) }">激活账
号</a>
</body>
</html>
id
为⽤户注册成功后⽣成的唯⼀标示,每次动态替换。
效果图如下:
发送邮件
public void sendRegisterMail(UserEntity user) {
Context context = new Context();
context.setVariable("id", user.getId());
String emailContent = templateEngine.process("emailTemplate", context);
MimeMessage message = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(user.getEmail());
helper.setSubject("注册验证邮件");
helper.setText(emailContent, true);
mailSender.send(message);
} catch (Exception e) {
logger.error("发送注册邮件时异常!", e);
}
}
上⾯代码封装了邮件发送的内容,注册成功后调⽤即可。
邮箱验证
当⽤户单击链接时请求
verifified()
⽅法,将⽤户的状态改为:
verifified
,表明邮箱已经得到验证。
@RequestMapping("/verified/{id}")
public String verified(@PathVariable("id") String id,ModelMap model) {
UserEntity user=userRepository.findById(id);
if (user!=null && "unverified".equals(user.getState())){
user.setState("verified");
userRepository.save(user);
model.put("userName",user.getUserName());
}
return "verified";
}
验证成功后,在⻚⾯中给出提示:
<h1>注册邮箱验证</h1>
<br/><br/>
<div class="with:60%">
<h3 th:if="${userName!=null}">邮箱验证成功,<a href="/toLogin">请登录!</a></h3>
<h3 th:if="${userName==null}">邮箱已经验证或者参数有误,请重新检查!<a href="/toRegiste
r">去注册</a></h3>
</div>
效果图如下:
Redis 使⽤、⾃定义 Filter
Redis
Session 管理
使⽤
Redis
管理
Session
⾮常简单,只需在配置⽂件中指明
Session
使⽤
Redis
,配置其失效时间。
spring.session.store-type=redis
# 设置 session 失效时间
spring.session.timeout=3600
数据缓存
为了避免⽤户列表⻚每⼀次请求都会查询数据库,可以使⽤
Redis
作为数据缓存。只需要在⽅法头部添加⼀个注
解即可,如下:
@RequestMapping("/list")
@Cacheable(value="user_list")
public String list(Model model,@RequestParam(value = "page", defaultValue = "0") Inte
ger page,
@RequestParam(value = "size", defaultValue = "6") Integer size) {
//⽅法内容
return "user/list";
}
⾃定义 Filter
我们需要⾃定义⼀个
Filter
,来判断每次请求的时候
Session
是否失效,同时排除⼀些不需要验证登录状态的
URL
。
启动时初始化⽩名单
URL
地址,如注册、登录、验证等。
// 将 GreenUrlSet 设置为全局变量,在启动时添加 URL ⽩名单
private static Set<String> GreenUrlSet = new HashSet<String>();
...
//不需要 Session 验证的 URL
@Override
public void init(FilterConfig filterconfig) throws ServletException {
GreenUrlSet.add("/toRegister");
GreenUrlSet.add("/toLogin");
GreenUrlSet.add("/login");
GreenUrlSet.add("/loginOut");
GreenUrlSet.add("/register");
GreenUrlSet.add("/verified");
}
...
//判断如果在⽩名单内,直接跳过
if (GreenUrlSet.contains(uri) || uri.contains("/verified/")) {
log.debug("security filter, pass, " + request.getRequestURI());
filterChain.doFilter(srequest, sresponse);
return;
}
...
uri.contains("/verifified/")
表示
URL
含有
/verifified/
就会跳过验证。
同时
Filter
中也会过滤静态资源:
if (uri.endsWith(".js")
|| uri.endsWith(".css")
|| uri.endsWith(".jpg")
|| uri.endsWith(".gif")
|| uri.endsWith(".png")
|| uri.endsWith(".ico")) {
log.debug("security filter, pass, " + request.getRequestURI());
filterChain.doFilter(srequest, sresponse);
return;
}
Session
验证:
String id=(String)request.getSession().getAttribute(WebConfiguration.LOGIN_KEY);
if(StringUtils.isBlank(id)){
String html = "<script type=\"text/javascript\">window.location.href=\"/toLogin\"
</script>";
sresponse.getWriter().write(html);
}else {
filterChain.doFilter(srequest, sresponse);
}
判断
Session
中是否存在⽤户
ID
,如果存在表明⽤户已经登录,如果不存在跳转到⽤户登录⻚⾯。
这样
Session
验证就完成了。
⻚⾯布局
现在需要在⽤户登录后的所有⻚⾯中添加版权信息,部分⻚⾯的头部添加⼀些提示信息,这时候就需要引⼊⻚⾯
布局,否则每个⻚⾯都需要单独添加,当⻚⾯越来越多的时候容出错,使⽤
Thymeleaf
的⽚段表达式可以很好
的解决这类问题。
我们⾸先可以抽取出公共的⻚头和⻚尾。
⻚头
<header th:fragment="header">
<div style="float: right;margin-top: 30px">
<div style="font-size: large">欢迎登录, <text th:text="${session.LOGIN_SESSION
_USER.getUserName()}" ></text>
! <a href="/loginOut" th:href="@{/loginOut}" style="font-size: small">退 出</a>
</div>
<div th:if="${session.LOGIN_SESSION_USER.getState()=='unverified'}" style="c
olor: red">请尽快验证您的注册邮件!</div>
</div>
</header>
根据上⾯代码可以看出⻚头做了以下⼏个事情:
- ⽤户登录后给出欢迎信息
- 提供⽤户退出链接
- 如果⽤户邮箱未验证给出提示,让⽤户尽快验证注册邮箱。
⻚尾
<footer th:fragment="footer">
<p style="color: green;margin: 60px;float: right">© 2018-2020 版权所有 纯洁的微笑</p
>
</footer>
⻚尾⽐较简单,只是展示出版权信息。
接下来需要做⼀个⻚⾯模板
layout.html
,包含标题、内容和⻚尾。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:fragment="common_layout(title
,content)">
<head>
<meta charset="UTF-8"></meta>
<title th:replace="${title}">comm title</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.css}"></link>
<link rel="stylesheet" th:href="@{/css/main.css}"></link>
</head>
<body>
<div class="container">
<th:block th:replace="${content}" />
<th:block th:insert="layout/footer :: footer" ></th:block>
</div>
</body>
</html>
这⾥定义了⼀个⽚段表达式
common_layout(title,content)
,同时在⻚⾯可以看到
<title th:replace="${title}">comm title</title>
和
<th:block th:replace="${content}" />
的两块作为⽚段表达式的参数,也就是说如果其他⻚⾯想使
⽤此⻚⾯的布局,只需要传⼊
title
和
content
两块的⻚⾯代码即可。
⻚⾯中使⽤了
th:block
,此元素作为⻚⾯的⾃定义使⽤不会展示到⻚⾯中,在⻚⾯模板的
head
中引⼊了两个
css
⽂件,也意味使⽤此⽚段表达式的⻚⾯同时会具有这两个
css
⽂件,在⻚⾯的最后将我们抽取的⻚⾯做完⻚
⾯⽚段引⼊。此模板⻚⾯并没有引⼊
Header
⻚⾯信息,因此我们只希望在列表⻚⾯展示⽤户的登录状态信息。
⽤户列表⻚引⼊模板
layout
示例:
<html xmlns:th="http://www.thymeleaf.org" th:replace="layout :: common_layout(~{::ti
tle},~{::content})">
<head>
<meta charset="UTF-8"/>
<title>⽤户列表</title>
</head>
<body>
<content>
<th:block th:if="${users!=null}" th:replace="layout/header :: header" ></th:
block>
...
⽤户列表信息
...
</content>
</body>
</html>
最主要有三块内容需要修改:
- html 头部添加 th:replace="layout :: common_layout(~{::title},~{::content})" 说明只有了 layout.html ⻚⾯的GitChat common_layout ⽚段表达式;
- <th:block th:if="${users!=null}" th:replace="layout/header :: header"></th:block> ⻚⾯引⼊了前⾯定义的 Header 信息,也就是⽤户登录状态相关内容;
- 提前定义好 title 和 content 标签,这两个⻚⾯标签会作为参数和定义的⻚⾯模板组合成新的⻚⾯。
效果图如下:
修改⽤户⻚⾯模板示例:
<html xmlns:th="http://www.thymeleaf.org"th:replace="layout :: common_layout(~{::titl
e},~{::content})" >
<head>
<meta charset="UTF-8"/>
<title>修改⽤户</title>
</head>
<body>
<content >
....
修改⻚⾯
....
</div>
</body>
</html>
效果图如下:
我们发现修改⻚⾯有版权信息,证明使⽤⽚段表达式布局成功,添加⽤户⻚⾯类似这⾥不再展示。
统⼀异常处理
如果在项⽬运⾏中出现了异常,我们⼀般不希望将这个信息打印到前端,可能会涉及到安全问题,并且对⽤户不
够友好,业内常⽤的做法是返回⼀个统⼀的错误⻚⾯提示错误信息,利⽤
Spring Boot
相关特性很容易实现此功
能。
⾸先来⾃定义⼀个错误⻚⾯
error.html
:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head lang="en" >
<meta charset="UTF-8" />
<title>500</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.css}"></link>
</head>
<body class="container">
<h1>服务端错误</h1>
请求地址:<pan th:text="${url}"></pan><br/>
错误信息:<pan th:text="${exception}"></pan>
</body>
</html>
⻚⾯有两个变量信息,⼀个是出现错误的请求地址和异常信息的展示。
创建
GlobalExceptionHandler
类处理全局异常情况。
@ControllerAdvice
public class GlobalExceptionHandler {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(Exception e, HttpServletRequest request)
throws Exception {
logger.info("request url:" + request.getRequestURL());
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", request.getRequestURL());
logger.error("exception:",e);
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
@ControllerAdvice
是⼀个控制器增强的⼯具类,可以在项⽬处理请求的时候去做⼀些额外的操作,
GitChat
@ControllerAdvice
注解内部使⽤
@ExceptionHandler
、
@InitBinder
、
@ModelAttribute
注解的⽅法应⽤到所有
的
@RequestMapping
注解⽅法。
@ExceptionHandler
注解即可监控
Contoller
层代码的相关异常信息。
我们修改代码在登录⻚⾯控制器中抛出异常来测试:
@RequestMapping("/toLogin")
public String toLogin() {
if (true)
throw new RuntimeException("test");
return "login";
}
启动项⽬之后,访问地址
http://localhost:8080/
,⻚⾯即可展示以下信息:
服务端错误
请求地址:http://localhost:8080/toLogin
错误信息:java.lang.RuntimeException: test
可以看出打印出来出现异常的请求地址和异常信息,表明统⼀异常处理成功拦截了异常信息。
Docker 部署
我们将⽤户管理系统
user-manage
复制⼀份重新命名为
user-manage-plus
,在
user-manage-plus
项⽬上添加
Docer
部署。
(
1
)项⽬添加
Docker
插件
在
pom.xml
⽂件中添加
Docker
镜像名称前缀:
<properties>
<docker.image.prefix>springboot</docker.image.prefix>
</properties>
plugins
中添加
Docker
构建插件:
GitChat
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Docker maven plugin -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
<!-- Docker maven plugin -->
</plugins>
</build>
(
2
)添加
Dockerfifile
⽂件
在⽬录
src/main/docker
下创建
Dockerfifile
⽂件:
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD user-manage-plus-1.0.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
注意
ADD
的是我们打包好的项⽬
Jar
包名称。
(
3
)部署
将项⽬
user-manage-plus
复制到安装好
Docker
环境的服务器中,进⼊项⽬路径下。
#打包
mvn clean package
#启动
java -jar target/user-manage-plus-1.0.jar
看到
Spring Boot
的启动⽇志后表明环境配置没有问题,接下来使⽤
DockerFile
构建镜像。
mvn package docker:build
构建成功后,使⽤
docker images
命令查看构建好的镜像:
[root@localhost user-manage-plus]# docker images
REPOSITORY TAG IMAGE ID
CREATED SIZE
springboot/user-manage-plus latest f5e23ce0ce7d
4 seconds ago 139 MB
springboot/user-manage-plus
就是我们构建好的镜像,下⼀步就是运⾏该镜像:
docker run -p 8080:8080 -t springboot/user-manage-plus
启动完成之后我们使⽤
docker ps
查看正在运⾏的镜像:
[root@localhost user-manage-plus]# docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
6e0ba131da6d springboot/user-manage-plus "java -Djava.secur..." 2 minutes
ago Up 2 minutes 0.0.0.0:8080->8080/tcp elastic_bartik
可以看到构建的容器正在在运⾏,访问浏览器
http://192.168.0.x:8080
,跳转到登录⻚⾯证明项⽬启动成功。
说明使⽤
Docker
部署
user-manage-plus
项⽬成功!
总结
我们⽤思维导图来看⼀下⽤户管理系统所涉及到的内容:
左边是我们使⽤的技术栈,右边为⽤户管理系统所包含的功能,通过这⼀节课的综合实践,我们了解到如何使⽤
Spring Boot
去开发⼀个完整的项⽬、如何在项⽬中使⽤我们前期课程所学习的内容。