Skip to content

登录功能实现

实际上,登录在逻辑上应该是"查询"操作,但是我们通常用post方法. 我们登录的结果(后端给前端返回的)往往有以下信息,我们可以封装成一个类.

/**
 * 封装登录的结果
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id;
    private String username;
    private String name;
    private String token;
}
其中token是后端给前端的令牌.我们后面再说. controller层
/*
    登录Controller
 */
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private EmpService empService;
    /**
     * 登录方法
     * @return 登录结果
     */
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("登录请求,参数:{}", emp);
        LoginInfo info = empService.login(emp);
        if (info == null) {
            return Result.error("用户名或密码错误");
        }
        log.info("登录成功,用户信息:{}", info);
        return Result.success(info);
    }

}
注意,由于Emp当中描述了员工的信息(包括用户名密码头像等等等等一系列信息),虽然前端只会传username和密码,但是还是可以正常接受的. Service层
    @Override
    public LoginInfo login(Emp emp) {
        //1.调用mapper查询员工信息
        Emp e = empMapper.selectByUsernameAndPassword(emp);
        //2.判断员工是否存在
        if(e!=null){
            log.info("登录成功,员工信息: {}", e);
            return new LoginInfo(e.getId(),e.getUsername(),e.getName(),"");
        }
        return null;
    }
token我们暂时不填. mapper层
    /*
        * 根据用户名和密码查询员工信息
     */
    @Select("select id, username,name from emp where username = #{username} and password = #{password}")
    Emp selectByUsernameAndPassword(Emp emp);
当然,上述方案是有问题的,因为前端完全可以直接访问域名/XXX 来访问页面,而根本不需要通过域名/login

登录校验

本质上,我们为了解决这个问题,就需要在用户访问后台主页面的时候进行登录校验,必须处于登录状态才能操作.

思路

后端存储一个登录标记,记录当前时刻有哪些用户处于登录状态. 在用户访问后端任何一个Controller服务之前,要先把请求发送给一个统一拦截的网关.网关会检验每个请求,是否有对应的登录标记.如果有,那么就放行.如果没有,那么就直接给前端返回信息要求重新登录. - 登录标记可以通过"会话技术"来解决. - 统一拦截可以通过过滤器Filter 或 拦截器Interceptor 解决.

会话技术

定义

用户打开浏览器,访问web服务器的资源,会话就建立了.有一方断开连接,会话就结束了.在一次会话中间,可以包括很多次的请求和相应.

会话跟踪

一种用来让服务器识别多次请求是否属于同一个浏览器的技术,这是用来在同一次会话的多次请求之间共享数据用的. 会话跟踪很重要.比如,如果需要在每次登录时使用验证码,这个验证码是需要后端传递给前端的.后端需要记住这次给前端传递了些什么,因为待会用户点击"登录"时会把这个验证码传给后端,后端需要把传来的验证码和之前记住的验证码进行比对.

解决方案

服务端会话跟踪技术:Session

令牌技术

Pasted image 20250628211947 三个自动: 比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。 服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

演示

Pasted image 20250628212039 访问/c1直接就能让浏览器得到这个cookie. 访问/c2就会把cookie发到服务器. 可以查看本地存的cookie: 前三个都是与idea有关的,第四个是我们设置的. 另外,访问同一个网址的时候,会把网址对应的所有的cookie都发过去 Pasted image 20250628212205

缺点:
  • 缺点:
  • 移动端APP(Android、IOS)中无法使用Cookie
  • 不安全,用户可以自己禁用Cookie(设置禁止访问第三方cookie就行)
  • Cookie不能跨域

    跨域: Pasted image 20250628213044

    • 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
    • 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
    • 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
    • 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
    • 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域
    • 区分跨域的维度(三个维度有任何一个维度不同,那就是跨域操作):

      • 协议
      • IP/协议
      • 端口

      举例: - http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]

      Session

      服务器端会话技术. Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的 Pasted image 20250628213348 登录之后,如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。 Pasted image 20250628213410 接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。 Pasted image 20250628213602 接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。 代码示例:

      @Slf4j
      @RestController
      public class SessionController {
      
          @GetMapping("/s1")
          public Result session1(HttpSession session){//声明session就行
              log.info("HttpSession-s1: {}", session.hashCode());
      
              session.setAttribute("loginUser", "tom"); //往session中存储数据,存储的值的名字是什么,值是什么.
              return Result.success();
          }
      
          @GetMapping("/s2")
          public Result session2(HttpServletRequest request){
              HttpSession session = request.getSession();
              log.info("HttpSession-s2: {}", session.hashCode());//前后的hashCode应该是一样的.
      
              Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
              log.info("loginUser: {}", loginUser);
              return Result.success(loginUser);//告诉前端username是什么
          }
      }
      
      前端结果: Pasted image 20250709231734 测试之后,显示结果: Pasted image 20250628213723 两次请求,获取到的Session会话对象的hashcode是一样的,就说明是同一个会话对象。而且,第一次请求时,往Session会话对象中存储的值,第二次请求时,也获取到了。 那这样,我们就可以通过Session会话对象,在同一个会话的多次请求之间来进行数据共享了。 - 缺点: - 服务器集群环境下无法直接使用Session - 移动端APP(Android、IOS)中无法使用Cookie - 用户可以自己禁用Cookie - Cookie不能跨域 - 为什么集群不能使用session? 这是微服务集群架构图: Pasted image 20250628213914 最终部署的时候都是以集群的形式来进行部署,也就是同一个项目它会部署多份。比如这个项目我们现在就部署了 3 份。 登录请求到达负载均衡服务器,将这个请求转给了第一台 Tomcat 服务器。此时假如又执行了一次查询操作,要查询部门的数据。这次请求到达负载均衡服务器之后,负载均衡服务器将这次请求转给了第二台 Tomcat 服务器.这样就出现不了问题的.

令牌技术(最常用)

就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串. 如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。 接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。 接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。 此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。 - 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

JWT令牌技术

  • JWT全称 JSON Web Token (官网:https://jwt.io/),定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
  • 简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
  • 自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
  • 简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。 jwt令牌是login之后产生的. #### JWT的组成:
  • (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
  • Pasted image 20250628214637
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
  • 其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
  • Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号(结尾的= 是一个补位符号,这个很容易在base64产生.)
  • 需要注意的是Base64是编码方式,而不是加密方式。
  • 我们可以看到,这是header和payload都是明文的.
  • 签名肯定是有个密钥的.

生成和校验

要想使用JWT令牌,需要先引入JWT的依赖:

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
可以调用工具包中提供的API来完成JWT令牌的生成和校验。工具类:Jwts
@Test
public void testGenJwt() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 10);
    claims.put("username", "itheima");

    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")//指定使用 HS256 签名算法,并传入签名密钥aXRjYXN0(这个密钥其实是ithema的base64编码)
        .addClaims(claims)//将前面定义的claims中的自定义信息添加到 JWT 中。
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))//设置 JWT 的过期时间,也就是说这里是12个小时候过期.这个最后会在payload里添加一个expjson字段,标识过期时间
        .compact();//完成 JWT 的构建,并将其转换为紧凑的字符串形式。

    System.out.println(jwt);
}
输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。 Pasted image 20250628215213 也就是说,只是签名了,并没有加密. 第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。 第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。 由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。 进行校验:
@Test
public void testParseJwt() {
    Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
        .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")
        .getBody();
    System.out.println(claims);
}
运行结果报错还可能是令牌过期了. {id=10, username=itheima, exp=1701909015} Pasted image 20250628215341 实际上,如果设置的时间过期,那么就

代码:

  1. 引入JWT工具类:在项目工程下创建 com.itheima.util 包,并把提供JWT工具类复制到该包下
    package com.itheima.util;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    
    import java.util.Date;
    import java.util.Map;
    
    public class JwtUtils {
    
        private static String signKey = "SVRIRUlNQQ==";
        private static Long expire = 43200000L;
    
        /**
         * 生成JWT令牌
         * @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. 完善 EmpServiceImpl中的 login 方法逻辑, 登录成功,生成JWT令牌并返回
    @Override
    public LoginInfo login(Emp emp) {
        Emp empLogin = empMapper.getUsernameAndPassword(emp);
        if(empLogin != null){
            //1. 生成JWT令牌
            Map<String,Object> dataMap = new HashMap<>();
            dataMap.put("id", empLogin.getId());
            dataMap.put("username", empLogin.getUsername());
    
            String jwt = JwtUtils.generateJwt(dataMap);
            LoginInfo loginInfo = new LoginInfo(emjpLogin.getId(), empLogin.getUsername(), empLogin.getName(), wt);
            return loginInfo;//这里的loginInfo就表示login成功之后,同时把令牌返回过去
        }
        return null;
    }
    
    测试的结果:Pasted image 20250714000135 Pasted image 20250628215722 我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。(当然,这是前端写的) Pasted image 20250628215732 Pasted image 20250628215747

过滤器 Filter

我们可以看到在后续的请求当中,都会在请求头中携带JWT令牌(token字段)到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。 - Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。(除了Filter之外的两个都不常用了.) - 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能 - 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。 - 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

快速入门

  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
    public class DemoFilter implements Filter {
        //滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("init ...");
        }
    
        //这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
            System.out.println("拦截到了请求...");
        }
    
        //- destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。
        public void destroy() {
            System.out.println("destroy ... ");
        }
    }
    
  • 第2步,配置过滤器 在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求
    @WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
    public class DemoFilter implements Filter {
        //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次,很不常用
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("init ...");
        }
    
        //拦截到请求时,调用该方法,可以调用多次
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
            System.out.println("拦截到了请求...");
        }
    
        //销毁方法, web服务器关闭时调用, 只调用一次.很不常用
        public void destroy() {
            System.out.println("destroy ... ");
        }
    }
    
    当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。(Springboot中使用这种传统开发的组件,必须这么做.)
    @ServletComponentScan //开启对Servlet组件的支持
    @SpringBootApplication
    public class TliasManagementApplication {
        public static void main(String[] args) {
            SpringApplication.run(TliasManagementApplication.class, args);
        }
    }
    
    注意事项:在过滤器Filter中,如果不执行放行操作,将无法访问后面的资源。 放行操作:chain.doFilter(request, response); Pasted image 20250714000639 注意专门创建了一个包;注意导入的位置. 放行操作: Pasted image 20250714000936

具体实现

大概清楚了在Filter过滤器的实现步骤了,那在正式开发登录校验过滤器之前,我们思考两个问题: 1. 所有的请求,拦截到了之后,都需要校验令牌吗 ? - 答案:登录请求例外,注册也是(不过我们这个项目没有注册,都是后台管理员手动加的.) 1. 拦截到请求后,什么情况下才可以放行,执行业务操作 ? - 答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果 Pasted image 20250628220615 401是前后端约定好的,表示没有登录,前端收到了之后,立刻就跳转到登录页面. 在 com.itheima.filter 包下创建TokenFilter,具体代码如下:

package com.itheima.filter;

import com.itheima.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.io.IOException;

/**
 * 令牌校验过滤器
 */
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;//因为我们知道这是httprequest
        HttpServletResponse response = (HttpServletResponse) resp;
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

        //2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){ //登录请求
            log.info("登录请求 , 直接放行");
            chain.doFilter(request, response);
            return;
        }

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

        //4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);//其实就是401.
            return;
        }

        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        //6. 放行。
        log.info("令牌合法, 放行");
        chain.doFilter(request , response);
    }

}

细节

  1. 在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。
    @WebFilter(urlPatterns = "/*") 
    public class DemoFilter implements Filter {
    
        @Override //初始化方法, 只调用一次
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("init 初始化方法执行了");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
            System.out.println("DemoFilter   放行前逻辑.....");
    
            //放行请求
            filterChain.doFilter(servletRequest,servletResponse);
            //资源访问结束后(也就是Controller都返回了之后),就会回到这里,
            System.out.println("DemoFilter   放行后逻辑.....");
    
        }
    
        @Override //销毁方法, 只调用一次
        public void destroy() {
            System.out.println("destroy 销毁方法执行了");
        }
    }
    
    效果: Pasted image 20250628221056
  2. 拦截路径 Pasted image 20250628221149
  3. 过滤器链 过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。 Pasted image 20250628221234 比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。 而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。 访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。 先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。
  4. 过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:
    • AbcFilter
    • DemoFilter 这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。

拦截器Interceptor

什么是拦截器? - 是一种动态拦截方法调用的机制,类似于过滤器。 - 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。 - 拦截器的作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。 Pasted image 20250628221504 在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。

基本使用

  1. 自定义拦截器 实现HandlerInterceptor接口,并重写其所有方法
    //自定义拦截器
    @Component
    public class DemoInterceptor implements HandlerInterceptor {
        //目标资源方法执行前执行。 返回true:放行    返回false:不放行
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("preHandle .... ");
    
            return true; //true表示放行
        }
    
        //目标资源方法执行后执行
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("postHandle ... ");
        }
    
        //视图渲染完毕后执行,最后执行.视图渲染只在前后端不分离的时候常用.这里了解一下即可.
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("afterCompletion .... ");
        }
    }
    
  2. 注册配置拦截器 在 com.itheima下创建一个包,然后创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法
    @Configuration  //表示这是一个配置类.但是实际上,底层就是compenent的封装.
    public class WebConfig implements WebMvcConfigurer {
    
        //自定义的拦截器对象
        @Autowired
        private DemoInterceptor demoInterceptor;
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
           //注册自定义拦截器对象
            registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
        }
    }
    
    测试结果: Pasted image 20250628222309 Pasted image 20250719151136

令牌校验拦截器.

在 com.itheima.interceptor 包下创建 TokenInterceptor.逻辑上只需要把filter的改一下就行.

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

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

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

        //4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        //6. 放行。
        log.info("令牌合法, 放行");
        return true;
    }

}
2. 配置拦截器
@Configuration  //打开这个,会发现里面也是@compoent
public class WebConfig implements WebMvcConfigurer {
    //拦截器对象
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

细节

在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor)
                .addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
                .excludePathPatterns("/login");//设置不拦截的请求路径
    }
}
Pasted image 20250628223239

执行流程

Pasted image 20250628223304 - 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。 - Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。 - 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。 - 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

对比

以上就是拦截器的执行流程。通过执行流程分析,大家应该已经清楚了过滤器和拦截器之间的区别,其实它们之间的区别主要是两点: - 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。 - 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。 另外,从代码上看,拦截器和过滤器都是在三层架构之外的.