SpringBoot拦截器

        我们之前所完成的强制登录功能,后端程序需要根据 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:本文的内容就到这里结束了,如果对你有所帮助的话就请一键三连哦!!!

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值