我们之前所完成的强制登录功能,后端程序需要根据 Session 来判断用户是否登录,但是实现方法是比较麻烦的,需要完成下面几个步骤:
1、需要修改每个接口的处理逻辑
2、需要修改每个接口的返回结果
3、接口定义修改,前段代码也需要跟着修改
所以需要一种更简单的方式,统一拦截所有的请求,并进行 Session 的校验的方式;该方式就是Spring 框架提供的核心功能之一:拦截器
1. 拦截器快速入门
1.1 什么是拦截器
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求,在指定的方法前后,根据业务需要执行预先设定的代码。
故此,允许开发人员提前预定义一些逻辑,在用户的请求、响应前后执行。也可以在用户请求前阻止其执行。
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息,如果有 -> 就放行,如果没有 -> 就进行拦截。
如下图所示:
简单来说拦截器就是整个系统开发人员为了整个系统的高速运行,提前将一些非法或者不友好的请求在系统内部设定为无效访问,拉入黑名单,禁止其访问,故此当一些无效或访问进行请求连接时,我们的系统阻止这些请求和系统连接,从而禁止访问;将结果提前扼杀在摇篮里;
接下来,我们来学习拦截器的基本使用。
1.2 拦截器的基本使⽤
拦截器的使⽤步骤分为两步:
1. 定义拦截器
2. 注册配置拦截器
自定义拦截器,实现 HandlerInterceptor 接口,并且重写所有方法,代码如下:
package com.example.book_manage_240827;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sound.sampled.Line;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
log.info("LoginInterceptor preHandle(目标方法执行前执行......)");
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor postHandle(目标方法执行后执行......)");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
log.info("LoginInterceptor afterHandle(目标方法执行后执行......)");
}
}
preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true: 继续执⾏后续操作; 返回false: 中断后续操作.
postHandle()⽅法:⽬标⽅法执⾏后执⾏
afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图, 暂不了 解)
1.2 注册配置拦截器
创建一个 WebConfig 类,实现WebMvcConfigurer接口,并重写addInterceptors方法(),代码如下:
浏览器访问 http://127.0.0.1:8082/book/getListByPage?currentPage=1,
观察日志:
可以看到,在执行目标方法前,就执行了 preHandle() 方法了;因为preHandle返回了true,才能执行执行目标方法,目标方法执行完后,执行 postHandle() 方法;最后才执行 afterCompletion() 方法。
现在我们把 preHandle() 方法的返回值修改成 false试试,页面如下:
观察打印结果:
因为 preHandle() 方法返回的是 false,所有没有打印 postHandle() 和afterCompletion() 方法,也就意味着中断了后续的操作 ;即程序拦截器拦截了请求, 服务器没有进⾏响应;
2. 拦截器详解
接下来我们学习拦截器的使用细节。拦截器的使用细节主要介绍两个部分:
1、拦截器的拦截路径配置
2、拦截器实现原理
2.1 拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效拦截。
我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求。也可以通过 excludePathPatterns() 指定不拦截哪些请求。上述代码中,我们配置的是 /** ,表示拦截所有的请求。
比如用户登录校验,我们希望可以对除了登录之外所有的路径生效,代码如下:
拦截所有路径的时候,登录页面:
不拦截登录要求的页面显示:
因为没有拦截它,也就不会执行那三个方法,即打印这三个方法的日志了。
下图是拦截器的一些其他拦截设置:
2.2拦截器执行流程
正常的调⽤顺序:
有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图:
1、添加拦截器后,执行 Controller 的方法之前,请求会先被拦截器拦截住,执行 preHadnle() 方法,这个方法需要返回一个布尔类型的值。如果返回true,就表示放行本次操作,继续访问 controller中的方法;如果返回false,则拦截(controller中的方法也不会执行)。
2、controller当中的方法执行完毕后,在回过来执行 postHandle() 这个方法 以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据。
3. 登录校验
学习了拦截器的基本操作之后,接下来我们通过拦截器来完成图书管理系统中的登录校验功能。
3.1 定义拦截器
从 session 中获取用户信息,如果 session 中不存在,就返回false,并设置http状态码为401,否则返回true。代码如下:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor preHandle(目标方法执行前执行).....");
//获取Session,判断Session中存储的userinfo信息是否为空
HttpSession session = request.getSession(true);
UserInfo userInfo = (UserInfo) session.getAttribute(Constant.USER_SESSION_KEY);
if(userInfo == null || userInfo.getId() <= 0) {
//用户未登录
response.setStatus(401);
return false;
}
return true;
}
}
http状态码401:Unauthorized
中文解释:未经过认证。指示身份验证是必需的,没有提供身份验证或身份验证失败。如果请求已经包含授权凭据,那么401状态码表示不接受这些凭据。
其中 request.getSession() 方法有下面两种形式,有参和无参两种方法;
其中,无参的 getSession() 方法默认 是 getSession(true);
getSession(true) 表示如果有Session,则返回Session;如果没有,则创建一个Session。
getSession(false) 表示 获取Session可能会是null。
3.2 注册配置拦截器
配置拦截器拦截所有的请求,除了 /user/login 和前端的页面,代码如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求
.excludePathPatterns("/user/login")//设置拦截器不拦截哪些请求路径
.excludePathPatterns("/css/**")//排除前端静态资源
.excludePathPatterns("/js/**")
.excludePathPatterns("/pic/**")
.excludePathPatterns("/**/*.html")
;
}
}
也可以定义一个链表,把要排除的路径都放进链表中,代码如下:
//也可以改成下面这样子
//定义一个链表,把要排除的路径都放进链表中
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求
.excludePathPatterns(excludePaths)//设置拦截器排除拦截的路径
;
}
}
http://127.0.0.1:8082/book/getListByPage?currentPage=1 http://127.0.0.1:8082/book/getListByPage?currentPage=1 浏览器访问:http://127.0.0.1:8082/book/getListByPage?currentPage=1,页面如下:
返回的响应状态码就是401。
图书页面进行访问:
登录页面进行访问,成功进入下面的图书列表页:
3.3 更改图书馆管理系统代码
用户登录接口
3.3.1 后端代码
我们已经定义和注册配置了拦截器,所以这里统一帮我们做了校验用户是否登录,没有登录就返回false(拦截),并且设置响应的状态码为401;用户已经登录了,获取到的Session能通过它拿到userInfo信息,userinfo也有对应的用户信息,就是用户登录了,则返回 true;就是上面定义拦截器的内容,这部分代码不变;
修改 controller的"/book/getListByPage" 接口,也就是删除一些逻辑判断,因为拦截器已经帮我们做了这些逻辑处理,代码如下:
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
log.info("获取图书列表, pageRequest:{}", pageRequest);
log.info("session", session);
//⽤⼾登录, 返回图书列表
PageResult<BookInfo> pageResult =
bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
进行测试:
登录成功后可以进入到图书列表页面;
3.3.2 前端代码
增加 error部分的代码,上面是进行判断了返回响应的信息,响应是不是空,已经响应的状态码是不是401,是的话就给他跳转到登录界面。且已经能测试成功;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图书列表展示</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/list.css">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script src="js/jq-paginator.js"></script>
</head>
<body>
<div class="bookContainer">
<h2>图书列表展示</h2>
<div class="navbar-justify-between">
<div>
<button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>
<button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>
</div>
</div>
<table>
<thead>
<tr>
<td>选择</td>
<td class="width100">图书ID</td>
<td>书名</td>
<td>作者</td>
<td>数量</td>
<td>定价</td>
<td>出版社</td>
<td>状态</td>
<td class="width200">操作</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="demo">
<ul id="pageContainer" class="pagination justify-content-center"></ul>
</div>
<script>
getBookList();
function getBookList() {
$.ajax({
type: "get",
url: "/book/getListByPage"+ location.search,
success: function (result) {
console.log("后端返回成功");
if (result.data != null && result.data.records != null) {
console.log("拿到参数")
var finalHtml = "";
var data = result.data;
for (var book of data.records) {
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" ' +
'value="'+book.id+'" id="selectBook" class="book-select"></td>';
finalHtml += '<td>'+book.id+'</td>';
finalHtml += '<td>'+book.bookName+'</td>';
finalHtml += '<td>'+book.author+'</td>';
finalHtml += '<td>'+book.count+'</td>';
finalHtml += '<td>'+book.price+'</td>';
finalHtml += '<td>'+book.publish+'</td>';
finalHtml += '<td>'+book.stateCN+'</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId='+book.id+'">修改</a>';
finalHtml += '<a href="javascript:void(0)"onclick="deleteBook('+book.id+')">删除</a>';
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: 100, //总记录数
pageSize: 10, //每页的个数
visiblePages: 5, //可视页数
currentPage: 1, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页面初始化和页码点击时都会执行
onPageChange: function (page, type) {
if (type != 'init') {
location.href = "book_list.html?currentPage=" + page;
}
}
});
}
},
error: function (error) {
console.log(error);
if(error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
//删除图书
$.ajax({
type: "post",
url: "/book/updateBook",
data: {
id: id,
status: 0
},
success: function () {
//重新刷新⻚⾯
location.href = "book_list.html"
}
});
}
}
// function batchDelete() {
// var isDelete = confirm("确认批量删除?");
// if (isDelete) {
// //获取复选框的id
// var ids = [];
// $("input:checkbox[name='selectBook']:checked").each(function () {
// ids.push($(this).val());
// });
// console.log(ids);
// alert("批量删除成功");
// }
// }
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
console.log(ids);
//批量删除
$.ajax({
type: "post",
url: "/book/batchDelete?ids="+ids,
success: function (result) {
console.log(result);
if (result == true) {
alert("删除成功");
//重新刷新⻚⾯
location.href = "book_list.html"
}
}
});
}
}
</script>
</div>
</body>
</html>
测试日志如下:
4. 适配器模式
4.1 适配器模式定义
适配器模式,也叫包装模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类也可以合作无间。
简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用。把两个不兼容的接口通过一定的方式使之兼容。
比如下图的两个接口,本身是不兼容的(参数类型不一样,参数个数不一样等等)::
可以通过适配器的方式,使之兼容:
4.2 适配器模式角色
Target:目标接口(可以是抽象类或接口),客户希望直接用的接口。
Adaptee:适配者,但是与 Target 不兼容。
Adapter:适配器类,此模式的核心。通过继承或者引用适配者的对象,把适配者转为目标接口。
client:需要使用适配器的对象。
4.3 适配器模式的实现
之前学习的 Slf4j 就使用了适配器模式,Slf4j 提供了一系列打印日志的 API, 底层调用的是 log4j 或者 logback 来打日志,我们作为调用者,只需要调用 Slf4j 的API 即可。
代码如下:
Target:(a)
public interface Slf4jLog {
void log(String message);
}
Adaptee:(b)
public class Log4j {
public void log4jPrint(String message) {
System.out.println("我是Log4j, 打印内容是: " + message);
}
}
Adapter:(c)
b 想使用 a的log接口,但是不行,因为自身打印的方法和a的不同。所以这时候就就需要使用适配器,定义一个类,通过继承 a接口,来实现它的方法,再引入 b,打印b的内容。
public class Log4jAdapter implements Slf4jLog{
private Log4j log4j;
public Log4jAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jPrint("我是适配器, 打印日志为: " + message);
}
}
client:
public class Main {
public static void main(String[] args) {
Slf4jLog slf4jLog = new Log4jAdapter(new Log4j());
slf4jLog.log("我是客户端");
}
}
运行该部分程序:
适配器就是起到一层包装的效果,因为接口和 Slf4jLog 不同,所以就使用适配器类 实现 Slf4jLog 接口,同时引用 Log4j,使用 Log4j 的打印方法(包装一层再给 Log4j 打印)。
最终还是给 Log4j 打印。适配器的作用就是给两个不同的接口,给其中一个接口进行包装一下,再丢给另一个接口进行使用。
可以看出,有了适配器模型,对已经定义好的接口,可以不进行修改,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行。
4.4 适配器模式应用场景
一般来说,适配器模式可以看做一种 “补偿模式”,用来 补救 设计上的缺陷。应用这种模式算是 “无奈之举”,如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了。
所以,适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以复用原有代码实现新的功能,比如版本升级等。
ps:本文的内容就到这里结束了,如果对你有所帮助的话就请一键三连哦!!!