第 5-8 课:综合实战客户管理系统(⼆)

客户管理系统需要考虑验证⽤户的注册邮箱是否正确,使⽤ 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 去开发⼀个完整的项⽬、如何在项⽬中使⽤我们前期课程所学习的内容。
发布了91 篇原创文章 · 获赞 20 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_34227896/article/details/103923297
5-8