JWT,Filter,Interceptor

发布于 8 天前 0 次阅读 2553 字 预计阅读时间: 12 分钟 JAVA


JWT令牌

其标准的格式如下: Header.Payload.Signature

1. 标头

标头Header 通常由两部分组成:令牌的类型(即 JWT)和所使用的签名算法(如 HS256 或 RS256)。

处理方式: 这段 JSON 会被使用 Base64Url 编码形成 JWT 的第一部分。

示例数据:

{
  "alg": "HS256",
  "typ": "JWT"
}

2. 有效载荷 (Payload)

有效载荷 (Payload)是令牌的核心,包含所谓的声明(Claims)。声明是关于实体(通常是用户)和其他数据的陈述。

示例数据:

{
  "sub": "1234567890",
  "name": "Suiyue",
  "admin": true
}
  • 处理方式: 同样使用 Base64Url 编码形成 JWT 的第二部分。

注意: Payload 只是编码,并没有加密。千万不要在 Payload 中存放密码、银行卡号等敏感信息,因为任何人拿到令牌都可以解码看到原始内容。

3. 签名

签名(Signature)是为了验证消息在传递过程中有没有被更改,并验证发送者的身份。

  • 签名生成方式:
    1. 将编码后的 Header 和编码后的 Payload 用 . 连接。
    2. 使用 Header 中指定的算法(如 HMAC SHA256)。
    3. 引入一个**只有服务器知道的密钥(Secret)**进行哈希计算。

使用

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
package site.suiyue.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

/**
 * JWT工具类,用于生成和解析JSON Web Token
 */
public class JwtUtils {

    // 签名密钥
    private static final String SECRET_KEY = "aXRjYXN0";

    // 默认过期时间:12小时(单位:毫秒)
    private static final long DEFAULT_EXPIRATION = 12 * 3600 * 1000L;

    // 私有构造方法,防止实例化
    private JwtUtils() {}

    /**
     * 生成JWT,使用默认过期时间(12小时)
     * @param claims 要放入JWT的声明(如用户id、用户名等)
     * @return JWT字符串
     */
    public static String generateJwt(Map claims) {
        return generateJwt(claims, DEFAULT_EXPIRATION);
    }

    /**
     * 生成JWT,指定过期时间
     * @param claims 要放入JWT的声明
     * @param expirationMillis 过期时间(相对于当前时间的毫秒数)
     * @return JWT字符串
     */
    public static String generateJwt(Map claims, long expirationMillis) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .addClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + expirationMillis))
                .compact();
    }

    /**
     * 解析JWT,返回Claims对象
     * @param token JWT字符串
     * @return Claims对象,包含JWT中的所有声明
     * @throws io.jsonwebtoken.ExpiredJwtException 如果JWT已过期
     * @throws io.jsonwebtoken.SignatureException 如果签名验证失败
     * @throws io.jsonwebtoken.MalformedJwtException 如果JWT格式错误
     */
    public static Claims parseJwt(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

注意事项

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
  • 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

Filter

使用

  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写所需方法。
  • 第2步,配置过滤器:
    Filter类上加 @WebFilter 注解,urlPatterns配置拦截资源的路径。
    引导类上加 @ServletComponentScan 开启Servlet组件支持。
package site.suiyue.filter;
@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 ... ");
    }
}
package site.suiyue;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan//开启对Servlet组件的支持
@SpringBootApplication
public class AitiliasApplication {

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

}

登录校验过滤器

package site.suiyue.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import site.suiyue.utils.JwtUtils;

import java.io.IOException;

@Slf4j
//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request=(HttpServletRequest)servletRequest;
        HttpServletResponse response=(HttpServletResponse)servletResponse;

        String url=request.getRequestURL().toString();

        if(url.contains("login")){
            log.info("登录请求,放行");
            filterChain.doFilter(request,response);
            return;
        }

        String jwt=request.getHeader("token");

        if(jwt==null||jwt.isEmpty()){
            log.info("令牌为空,返回错误结果");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        try{
            JwtUtils.parseJwt(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("解析令牌失败");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        log.info("令牌合法,放行");
        filterChain.doFilter(request,response);
    }

}

为何强转?

为什么将ServletRequest转为为HttpServletRequest

因为 ServletRequest 接口里根本没有 getHeader()getRequestURL() 这些方法。所以需要将ServletRequest强转为HttpServletRequest

是否安全?

安全,因为在现有的 Java Web 开发(尤其是 Spring Boot)中,底层的 Web 容器(如 Tomcat)分发过来的请求 100% 都是 HTTP 请求request底层的实现类实际上就是 RequestFacade(Tomcat 对 HttpServletRequest 的实现)。

拦截路径

拦截路径urlPatterns值含义
拦截具体路径/login只有访问 /login 路径时,才会被拦截
目录拦截/emps/*访问/emps下的所有资源,都会被拦截
拦截所有/*访问所有资源,都会被拦截

过滤器链

执行顺序:

过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:

  • AbcFilter
  • DemoFilter

这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。

Interceptor

使用

  • 第一步,实现HandlerInterceptor接口,并重写所需方法,定义拦截器
  • 第二步,注册/配置拦截器,在 site.suiyue下创建interceptor包,然后创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法
package site.suiyue.interceptor;
//自定义拦截器
@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 .... ");
    }
}
@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

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

登录校验拦截器

定义拦截器

package site.suiyue.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import site.suiyue.utils.JwtUtils;

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

        if (url.contains("login")) {
            log.info("登录请求,放行");
            return true;
        }

        String token = request.getHeader("token");

        if (!StringUtils.hasLength(token)) {
            log.info("令牌为空,401");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        try {
            JwtUtils.parseJwt(token);
        } catch (Exception e) {
            log.info("令牌解析异常");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            e.printStackTrace();
            return false;
        }

        log.info("通过,放行");
        return true;
    }
}

注册/配置拦截器

package site.suiyue.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.suiyue.interceptor.TokenInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    //拦截器对象
    @Autowired
    private TokenInterceptor tokenInterceptor;

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

拦截路径详解

调用addPathPatterns("要拦截路径")方法,就可以指定要拦截哪些资源。
调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

@Configuration  
public class WebConfig implements WebMvcConfigurer {

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

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor)
                .addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
                .excludePathPatterns("/login");//设置不拦截的请求路径
    }
}
拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配 /depts/1
/**任意级路径能匹配/depts,/depts/1,/depts/1/2
/depts/*/depts下的一级路径能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意级路径能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

执行流程

  • 当我们打开浏览器来访问部署在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:
    Filter类上加 @WebFilter 注解
    urlPatterns配置拦截资源的路径
    引导类上加 @ServletComponentScan 开启Servlet组件支持。


    配置Interceptor:
    在 site.suiyue下创建config包,然后创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,将对应的拦截器注入(@Autowired),并重写 addInterceptors 方法,设置拦截路径通过:registry.addInterceptor(刚刚注入的拦截器).addPathPatterns("拦截路径");
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源