过滤器与拦截器 - 登录校验与登录认证(JWT令牌技术)

登录校验与登录认证

一、登录认证

1.1 基础登录功能

接口

@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;

//  用Emp对象接收用户名和密码,里面将属性封装好了
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        log.info("员工登录{}",emp);
        Emp e = empService.login(emp);

        return e!=null? Result.success() :Result.error("用户名或密码错误");
    }
}

SQL

@Select("select  * from emp where username =#{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);

1.2 会话技术

1.2.1 介绍

会话:用户打开浏览器,访问web服务器资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。

比如我们打开浏览器与Web服务器建立连接。首先访问login接口,再访问depts接口,最后访问emps接口。只要浏览器和服务器都没有关闭,那么这三次请求都是在一次会话中完成的。

关闭服务器,所有的会话都会关闭

关闭当前浏览器,当前会话结束

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

http请求是无状态的,下一次请求并不会携带上一次请求的数据,每一次请求都是相互独立额,也就保证了http协议它的效率是比较高的。由于是无状态的,我们也无法确定两次请求是否来自于同一个浏览器,是否来自于同一个会话。此时同一个会话的多个请求之间是没有办法共享数据的。

我们要想解决这个问题,就需要会话跟踪技术。

会话跟踪方案

  • 客户端会话跟踪技术:Cookie 存储在客户端浏览器中
  • 服务端会话跟踪技术:Session 存储在服务器当中
  • 令牌技术

Cookie 与Session 是传统web开发当中所提供的的两种会话跟踪技术,而当前企业开发中最主流的是令牌技术

1.2.2 方案一 Cookie

1.2.2.1 基本介绍

存储在客户端浏览器,使用Cookie跟踪对话,就可以在浏览器第一次发起请求来请求浏览器的时候设置一个Cookie。

我们在Cookie中可以存储一些信息,比如存储用户名,用户id。服务端在给客户端响应数据的时候会自动将Cookie响应给浏览器。

浏览器接收到服务端响应的Cookie后将其存储在浏览器本地。

之后客户端每次发请求都会将本地存储的Cookie携带到服务端。我们在服务端就可以获取用户的信息,也可以判断Cookie是否存在。

如果不存在说明这个客户端之前没有访问登录接口;如果存在说明这个客户端之前已经登录完成了。

这样我们就能在同一次会话的不同请求之前来共享数据

三个自动

  • 服务器自动将Cookie响应给浏览器
  • 浏览器接收到响应回来的数据之后会自动的将Cookie存储在浏览器本地
  • 后续请求中浏览器会自动将Cookie携带到服务端

为什么上面这个三个时自动进行的呢?

因为Cookie是http协议支持的内容,各大浏览器厂商都支持了这一标准。

在HTTP协议中,提供了响应头(setCookie)和一个请求头

HTTP相关技术文档

https://cloud.tencent.com/developer/doc/1117

服务端在给浏览器响应Cookie的时候是以哪种方式响应回去的?

直接设置了一个响应头Set-Cookie,请求头所对应的数据就是Cookie对应的值。

“name”就是Cookie的名称,“value”就是Cookie的值

此响应头返回给浏览器,浏览器会自动解析响应头

image-20230516111420677

优点

  • HTTP 协议中支持的技术

    缺点

  • 移动端APP无法使用Cookie

  • 不安全,用户可以自己禁用Cookie

  • Cookie不能跨域(协议、IP/域名、端口号,这三者有一个不同就是跨域)

1.2.2.2 服务端向浏览器响应Cookie
//设置Cookie - 服务器要给浏览器响应数据
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
    response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
    return Result.success();
}

效果图

image-20230516112139072

自动保存在下图位置

image-20230516112255691

1.2.2.3 浏览器向服务端请求携带Cookie

服务端解析从浏览器向服务端请求携带的Cookie

//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    for (Cookie cookie : cookies) {
        if(cookie.getName().equals("login_username")){
            System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
        }
    }
    return Result.success();
}

1.2.3 方案二 Session

1.2.3.1 基本介绍

服务器端会话跟踪技术,存储在服务器端,底层借助Cookie实现。

浏览器在第一次请求服务器时,我们可以直接在服务器当中获取到会话对象Session(第一次请求会话对象Session是不存在的,服务器会自动创建一个会话对象),每一个会话对象都有一个id。

之后服务端再给浏览器响应数据的时候,会将Session的id通过Cookie响应给浏览器,其实就是在响应头重增加了一个Set-Cookie。

在这里Cookie的名字是固定的,就是JSESSIONID,代表的就是服务端会话对象Session的id。

浏览器接收到数据之后会自动将这个Cookie存储在浏览器本地,之后再请求服务端时都会将Cookie数据获取出来并且携带到服务端。

浏览器拿到JSESSIONID也就是Session的id后,会从众多会话对象Session中找到当前请求对应的绘画对象Session。

找到对应Session之后,就可以实现在同一次会话的多次请求之间来共享数据

image-20230516114347168

优点

  • 存储在服务端,安全

缺点

  • 服务器集群环境下无法直接使用Session
  • Cookie的缺点(底层Cookie实现)
1.2.3.2 服务端向浏览器响应Session
//  往HTTPSession中存储值
//  服务器会判断当前这次请求对应的会话对象Session是否存在,
//      如果不存在会新创建一个Session,如果存在会获取当前这一次请求对应的Session
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());//HttpSession-s1: 1750219908

        session.setAttribute("loginUser", "tom"); //往session中存储数据
        return Result.success();
    }

Set-Cookie

image-20230516115318650

Application

image-20230516115358133

1.2.3.3 浏览器向服务端请求携带Session
//  这个地方我们可以声明HttpSession对象,也可以使用HttpServletRequest对象
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();//拿到当前这次请求对应的会话对象
        log.info("HttpSession-s2: {}", session.hashCode());  //HttpSession-s2: 1750219908

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        log.info("loginUser: {}", loginUser);  //loginUser: tom
        return Result.success(loginUser);
    }

image-20230516115843757

注意看上面的几张图,页面中完全没有涉及到“loginUser”的存储,那为什么控制台还能输出“loginUser: tom”?

​ Session存储在服务器端,每次请求浏览器都会写到Cookie,并且有JSESSIONID,服务端获取到之后就可以解析,确定是哪个Session对象

而且观察控制台SessionID,确实是同一个

image-20230516120147814

二、JWT 令牌技术 - 主流

用户身份表示

存储在客户端。不用担心安全问题,浏览器携带令牌请求时,服务端会进行校验。

无效的,返回错误结果;有效的,访问对应权限。

优点

  • 支持PC端、移动端
  • 解决集群环境下的认证问题
  • 减轻服务器端存储压力

缺点

  • 需要自己实现

2.1 基本介绍

官方网站JSON Web Tokens - jwt.io

简洁、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息都是可靠的。

组成:

  • 第一部分:Header(头),记录令牌类型、签名算法等。

    例如:[“alg”:“HS256”,“type”:"WT”)

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。

    例如:“id”."1"“username”."Tom”

  • 第三部分: Signature(签名),防止Token被篡改、确保安全性。

    ​ 将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

    ​ 第三部分字符的由来不是Base64编码方式生成的,而是前面指定的签名算法

image-20230516122822444

JSON数据格式是怎么变成一些字符的呢?

通过Base64编码方式

应用场景

​ 登录认证。

① 用户登录成功后,服务端生成一个JWT令牌,并传输给前端。

②前端拿到JWT令牌之后会将其存储起来,之后前端的每一次请求都会将JWT令牌携带到服务端,服务端会对请求进行统一拦截,拦截之后先判断有没有把这个令牌带过来。

​ 如果没有令牌就拒绝访问;若有令牌但是无效仍然拒绝访问;若有效则直接放心,并处理对应请求

2.2 JWT令牌 生成

2.2.1 Maven

无论生成和校验都需要下面的工具类

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

2.2.2 生成JWT令牌

签名算法具体有哪些,可以查看官网

image-20230516124444272

    /**
     * 生成JWT
     */
    @Test
    public void testGenJwt() {
        Map<String, Object> map = new HashMap<>();
        map.put("id", 1);
        map.put("name", "tom");

//      链式编程 - Jwt令牌在生成的时候所需要设置的一些参数
        String jwt = Jwts.builder()
//              存储在第一个部分         参数一 数字签名算法  参数二 秘钥
                .signWith(SignatureAlgorithm.HS256, "zhangjingqi")
//               JWT令牌所存储的内容(自定义数据,存储在第二个部分,原始自定义数据是JSON格式)
//               可以是Map集合,也可以是Claims对象
                .setClaims(map)
//               设置令牌有效期 - 一个小时后过期, 因为是毫秒,3600*1000代表一个小时
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
//               调用compact会有一个String返回值,就是JWT令牌
                .compact();

        System.out.println(jwt);//eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NDIxNjUzNX0.s33vECyehznKMrbDqD1Pdx-DrHWkscdyNeWmLnY-ArU

    }

将生成的JWT放在官网进行解码,和我们的数据一模一样

image-20230516125816351

2.3 JWT令牌 校验

Jwt令牌生成后,不论我们改哪部分的字符,在解析的时候都会报错,所以Jwt令牌是很安全的。

令牌时间过期之后也不能访问

    @Test
    public void testParseJwt(){
        Claims claims = Jwts.parser()
//               指定签名秘钥
                .setSigningKey("zhangjingqi")
//               解析JTW令牌
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NDIxNjUzNX0.s33vECyehznKMrbDqD1Pdx-DrHWkscdyNeWmLnY-ArU")
//               拿到了我们自定义的内容,也就是Jwt令牌的第二个部分
                .getBody();
        System.out.println(claims); //{name=tom, id=1, exp=1684216535}
    }

注意事项

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的(编码和解析时秘钥必须相同

  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效,令牌非法

2.4 登录成功下发令牌

  • 令牌生成: 登录成功后,生成JWT令牌,返回给前端
  • 令牌校验,在请求到达服务端后,对令牌进行统一拦截、校验

用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为token,值为登录时下发的JWT令牌

2.4.1 封装工具类

将上面编码和解码汇编成一个工具类供我们使用

public class JwtUtils {

    private static String signKey = "zhangjingqi";
    private static Long expire = 43200000L;

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

2.4.2 发放JWT令牌

//  用Emp对象接收用户名和密码,里面将属性封装好了
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        log.info("员工登录{}",emp);
        Emp e = empService.login(emp);

//      登陆成功,生成令牌并下发令牌
        if(e!=null){
            Map<String, Object> claims = new HashMap<>();
            claims.put("id",e.getId());
            claims.put("name",e.getName());
            claims.put("username",e.getUsername());
//          生成令牌,员工信息已经在里面了
            String jwt = JwtUtils.generateJwt(claims);
            return Result.success(jwt);
        }

//      登录失败,返回错误信息
        return Result.error("用户名或密码错误");
    }

image-20230516141020710

网页联调

接收编码后,前端人员将其存储到Local Storage中

image-20230516142415731

这里面的Key “tlias_token”是前端自己定义的

image-20230516142556341

随便抓取一个请求,看此时是否携带Jwt令牌

显然是存在的。这个地方的实现是前端完成的,后端不需要管

image-20230516142855990

三、 Filter

Filter过滤器,是javaWeb三大组件(Servlet、Filter、Listener)之一

3.1 基本介绍

概念:

  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能

​ ① 想要访问服务器中的某些资源,必须先经过Filter过滤器

​ ② 再此处进行一些操作,完成之后进行放行,访问对应的资源

​ ③ 资源访问完毕,最后再回到过滤器,然后再给浏览器响应对应的数据

  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

​ 如果没有过滤器,我们需要在每一个接口中编写登录校验逻辑。

如果登录了,放行去访问对应的信息,如果没有登录,直接在Filter中返回错误信息,不再访问后面的请求

3.2 Filter 快速入门

  • 定义Filter:定义一个类,实现Filter接口,并重写其所有方法

    但是开发中我们一般只实现doFilter这个方法,其他两个方式使用默认实现即可

//  定义Filter,并重写三个方法
//  在web服务器启动的时候,会自动创建Filter过滤器对象
//不配置的话过滤器不会生效,urlPatterns表示拦截什么样的请求,/*代表拦截所有请求
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {

//  初始化方法,过滤器创建完毕之后会自动调用init方法,只会调用一次(只会在创建时调用一次)
//  一般在这里完成一些资源及环境的准备操作
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("init 初始化方法执行了");
    }


//  每一次拦截到请求都会调用的方法,最为重要的方法,是会被调用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        System.out.println("拦截方法执行,拦截到了请求 ...");
//      往下放行去访问对应的资源,如果不放行,页面获取不到对应数据(接口资源是获取不到的)
        chain.doFilter(request, response);
    }

//  销毁方法。服务区关闭时调用,只调用一次
//  一般在这里完成资源的释放和环境的清理操作
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("destroy 销毁方法执行了 ");
    }

}
  • 配置Filter:Filter类上添加@WebFilter注解,配置拦截资源路径。引导类上添加@ServletComponentScan注解开启Servlet组件支持
//Filter是javaweb三大组件之一,不是Spring提供的,如果想要使用三大组件,需要添加这个注解
@ServletComponentScan
@SpringBootApplication
public class SpringbootWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebApplication.class, args);
    }

}

启动程序,观察控制台

init方法只会执行这一次

image-20230516154156916

随意访问几个接口,观察控制台

image-20230516154417765

3.3 执行流程

image-20230516160151214

当我们的过滤器拦截到请求之后我们需要完成一个非常重要的操作,那就是放行。

chain.doFilter(request, response);

在过滤器放行之前我们可以执行一段逻辑。

”放行“就是让其去访问对应的web资源,访问完过滤器之后还会回到过滤器当中,执行“放行”语句后面的代码,执行完毕之后再给浏览器响应数据

chain.doFilter(request, response);
System.out.println("执行放行后逻辑 ...");
  • 放行后访问对应资源,资源访问完成后还会回到Filter中吗?

​ 会

  • 如果回到Filter中,是重新执行还是执行放行后的逻辑?

​ 执行放行后的逻辑

3.4 拦截路径

在快速入门中配置的是 /*,代表拦截所有请求

@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {}
拦截路径urlPatterns含义
拦截具体路径/login只访问/login路径时才会被拦截
目录拦截/emps/*访问/emps下的所有资源,都会被拦截
拦截所有/*访问所有资源,都会被拦截

3.5 过滤器链

  • 一个web应用中,可以配置多个过滤器,多个过滤器形成了一个过滤器链。

​ 过滤器链中的过滤器会一个一个的执行,第一个放行之后会执行第二个,依次推,最后一个过滤器执行完后会访问对应请求。

过滤器链中最后一个过滤器放行的话,会放行到web资源当中来访问web资源

​ 访问完资源后,是倒着进行的,先执行最后一个过滤器,再倒数第二个…

image-20230516161012600

  • 注解配置Filter,优先级是按照过滤器类型(字符串)的自然排序

创建新的过滤器

@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Abc拦截方法执行,拦截到了请求 ...");

        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("Abc执行放行后逻辑 ...");
    }
}

控制台输出:(我们之前创建了一个DemoFilter过滤器)

Abc拦截方法执行,拦截到了请求 …
Demo拦截方法执行,拦截到了请求 …

Demo执行放行后逻辑 …
Abc执行放行后逻辑 …

3.6 登录校验过滤器

备注说明:

用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为token,值为登录时下发的JWT令牌。

如果检测到用户未登录,则会返回如下固定错误信息。

  • 所有的请求,拦截到之后,都需要校验令牌吗?

​ 登录请求例外

  • 拦截到请求后,什么情况下可以放行,执行业务操作?

​ JWT令牌存在且令牌校验通过(合法),否则都会返回未登录错误结果

3.6.1 实现思路

  • 获取请求url
  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  • 获取请求头中的令牌(token)
  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  • 解析token,如果解析失败,返回错误结果(未登录)
  • 放行

3.6.2 代码实现

在过滤器当中为什么要把ServletRequest类型强转成HttpServletRequest类型?

在Java中,ServletRequest是一个接口,它是由Servlet容器提供的。HttpServletRequest接口则是ServletRequest接口的子接口,它包含了一些用于HTTP协议的方法和属性。在Java Web应用程序中,Servlet容器实现了ServletRequest和HttpServletRequest接口,并使用HttpServletRequest实现了HTTP协议相关的逻辑。

在开发Web应用程序时,Servlet容器将在每个客户端请求到达时创建一个ServletRequest对象并将其传递给请求处理器。由于具体的实现是由Servlet容器提供的并且通常是HttpServletRequest,因此在编写Servlet处理器时,我们通常将ServletRequest对象强制转换成HttpServletRequest对象,以便能够调用提供的HTTP协议相关方法。

因此,在过滤器中,如果我们需要使用HttpServletRequest接口中特定的HTTP协议相关方法,我们需要将ServletRequest对象强制转换成HttpServletRequest对象。这样我们才能够在处理ServletRequest对象时,使用HttpServletRequest中更多的方法和属性。

注意!!!将刚刚测试的Filter注释掉,只留这一个过滤器!!!!!!!!

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//      ServletRequest、ServletResponse是父类,
//      请求对象与响应对象
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //  TODO 1.获取请求url
        String requestURL = request.getRequestURL().toString(); //不toString就是StringBuffer类型
        log.info("请求的url:{}", requestURL);

        //  TODO 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if (requestURL.contains("/login")){
            log.info("登录操作,放行...");
            filterChain.doFilter(request, response);
//           登录操作不需要执行下面的逻辑,直接结束此过滤器即可
            return;
        }

        //  TODO 3.获取请求头中的令牌(token)
        String token = request.getHeader("token");

        //  TODO 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
        if (!StringUtils.hasLength(token)) { //spring当中的工具类
//            说明字符串为null,返回错误结果(未登录)
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
//          手动转JSON
            String errorJson = JSON.toJSONString(error);
//          response.getWriter()获取输出流,write()直接将数据响应给浏览器
            response.getWriter().write(errorJson);
            return;
        }

        //  TODO 5.解析token,如果解析失败,返回错误结果(未登录)
//      说明存在令牌,校验
        try{
            Claims claims = JwtUtils.parseJWT(token);
        }catch (Exception e){ // 出现异常代表着解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
//          手动转JSON
            String errorJson = JSON.toJSONString(error);
//          response.getWriter()获取输出流,write()直接将数据响应给浏览器
            response.getWriter().write(errorJson);
            return;
        }
//       到这里说明令牌解析成功,直接放行
        //  TODO 6.放行
        log.info("令牌合法,放行");
        filterChain.doFilter(request, response);
    }
}

很完美

image-20230516182613212

四、Interceptor - 拦截器

之前做的笔记: Springboot——拦截器_springboot 拦截器_

概念:是一种动态拦截方法调用调用机制,类似过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行(拦截请求的)

作用: 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码

image-20230516184503393

4.1 入门

在测试之前可以把过滤器关掉,否则一直验证token比较麻烦

  • 定义拦截器,实现HandlerInterceptor接口,并且重写其所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//  目标资源方法执行前执行(Controller方法执行之前), true:放行, false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle......");
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        return true;
    }

//  目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle.....");
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

//  视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion......");
    }
}
  • 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**");
    }
}

4.2 详解

4.2.1 拦截路径

addPathPatterns 指定拦截哪些路径

excludePathPatterns 执行不拦截哪些不经

registry.addInterceptor(loginCheckInterceptor)
        .addPathPatterns("/**")
        .excludePathPatterns("/login");
拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配/depts/1
/**任意级路径能匹配/depts/1,/depts,/depts/1/2
/depts/*/depts下一级路径能匹配/depts/1,不能匹配/depts,/depts/1/2,/depts
/depts/**/depts下的任意路径能匹配/depts,/depts/1,/depts,/depts/1/2

4.2.2 执行流程

① 当浏览器向Web服务器发送请求时,我们所设定的过滤器会拦截到这一请求。

② 过滤器先执行放行前逻辑,在此处决定是否放行

③ 放行之后进入到Spring环境中,进入DispatcherServlet

​ 在请求响应时说道,tomcat服务器并不识别我们所编写的controller程序,但是他是识别Servlet程序的,因为tomcat是一个Servlet程序。

​ 而在SpringWeb当中提供了一个非常核心的Servlet,我们叫做DispatcherServlet前端核心控制器,所以请求会先进入到DispatcherServlet,请求由DispatcherServlet再传给Controller

如果设置了拦截器的话,DispatcherServlet在传给Controller之前需要先被拦截器拦截住

④ 拦截器拦截到,先进行preHandle,决定是否放行,如果放行便访问Controller层方法

⑤Controller层方法完成之后,再执行postHandler方法已经afterCompletion方法

⑥返回给DispatcherServlet

⑦最终执行放行后逻辑

⑧最终给浏览器响应数据

image-20230517102927383

4.2.3 过滤器与拦截器的区别

  • 接口规范不同:过滤器实现Filter接口,而拦截器需要实现HandlerInterceptor接口

  • 拦截范围不同:过滤器Filter会拦截所有资源,而Interceptor只会拦截Spring环境中的资源,过滤器拦截范围更大

4.3 登录校验拦截器

4.3.1 实现思路

与过滤器思路一模一样

  • 获取请求url
  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  • 获取请求头中的令牌(token)
  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  • 解析token,如果解析失败,返回错误结果(未登录)
  • 放行

4.3.2 代码实现

测试之前记得吧过滤器注解注释掉

 //  目标资源方法执行前执行(Controller方法执行之前), true:放行, false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //  TODO 1.获取请求url
        String requestURL = request.getRequestURL().toString(); //不toString就是StringBuffer类型
        log.info("请求的url:{}", requestURL);

        //  TODO 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if (requestURL.contains("/login")) {
            log.info("登录操作,放行...");
            return true;
        }

        //  TODO 3.获取请求头中的令牌(token)
        String token = request.getHeader("token");

        //  TODO 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
        if (!StringUtils.hasLength(token)) { //spring当中的工具类
//            说明字符串为null,返回错误结果(未登录)
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
//          手动转JSON
            String errorJson = JSON.toJSONString(error);
//          response.getWriter()获取输出流,write()直接将数据响应给浏览器
            response.getWriter().write(errorJson);
            return false;
        }

        //  TODO 5.解析token,如果解析失败,返回错误结果(未登录)
//      说明存在令牌,校验
        try {
            Claims claims = JwtUtils.parseJWT(token);
        } catch (Exception e) { // 出现异常代表着解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
//          手动转JSON
            String errorJson = JSON.toJSONString(error);
//          response.getWriter()获取输出流,write()直接将数据响应给浏览器
            response.getWriter().write(errorJson);
            return false;
        }
//       到这里说明令牌解析成功,直接放行
        //  TODO 6.放行
        log.info("令牌合法,放行");
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        return true;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我爱布朗熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值