JWT令牌,过滤器filter和拦截器Interceptor的使用

195 阅读7分钟

1.JWT令牌

1.1 介绍

JWT全称 JSON Web Token (官网:jwt.io/ ),定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。 JWT的组成:

  • 第一部分Header(头):记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
  • 第二部分Payload(有效载荷):携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。 在生成JWT令牌时,会先对原始的json格式进行一次base64的编码,这样就可以将json格式转换为字符串然后获取令牌。

1.2 令牌的生成与校验

在生成令牌前,首先要加入令牌依赖:

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

有了依赖之后,就可以调用依赖中的API完成生成和校验,使用Jwts工具类

  • 生成的测试代码:
@Test
public void testGenJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put("id",10);
    claims.put("username","zhangsan")
    
    //signwith():jwt官网提供的jwt生成方式
    //setExpiration:密钥存活时间
    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
        .compact();
    //输出jwt密钥
    System.out.println(jwt);
}
  • 校验的测试代码:
@Test
public void testParseJwt(){
    Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
        .parseClaimsJws("这里写生成测试代码打印的jwt")
        .getBody()
        
    System.out.println(claims);
}
//理想输出:```
{id=10, username=zhangsan, exp=(过期时间的毫秒值)}

篡改令牌中的任何一个字符,在对令牌进行解析时都会报错,所以JWT令牌是非常安全可靠的。

1.3 jwt令牌在登录时的应用举例

在我们项目的utils包下建立jwt的JwtUtils类工具:

public class JwtUtils {

    private static String signKey = "SVRIRUlNQQ==";
    private static Long expire = 24 * 60 * 60 * 1000L;

    /**
     * 生成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;
    }
}

这里代码不多介绍,其实就是将上面的两个已经通过测试的生成与校验方法写入自定义工具类中。 接下来只需要在业务层完成根据接收到的登录信息来生成jwt令牌并注入令牌到前端,每一个请求的请求头中都会多出一个名为token的jwt令牌,再结合过滤器或者拦截器就可以拦截不合法的访问了。

这里说一个与本内容有关但关系不大的小tips:当我们生成令牌前通常需要根据持久层的sql语句先拿到与输入的账户相同用户名的账户验证登录信息,在从数据库取这个信息时,可以用select * from tablename where username = #{username} limit 1的sql语句提高查找的效率,因为考虑到username在数据库的唯一性,这样的语句可以提早结束语句的查询减少方法消耗的时间。值得一提的是在拿到数据信息后,不要将password这种敏感的信息写入生成jwt令牌的claims中,这是不被规范允许的。

接下来介绍过滤器与拦截器以及他们如何结合的jwt令牌来禁止非法访问。

2. 过滤器filter

2.1 filter介绍

  • 过滤器是JavaWeb的三大组件(Servlet,Filter,Listener)之一
  • 过滤器可以把对资源的请求拦截下来,从而添加想要实现的功能:一般完成登录校验,统一编码处理,敏感字符处理等功能。

2.2 fliter的基本使用操作

  1. 定义过滤器:
@WebFilter("/*")
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 ... ");
    }
}

其中最重要的是doFilter方法的重写,拦截到请求后,需要进行的操作,如判断jwt令牌是否正确都在doFilter方法中执行,如果验证通过了,执行放行操作chain.doFilter(request, response)即可放行,网页就正常响应了。如果验证不通过,则会报错,这里也可以在doFilter方法中去自定义报错的状态码。

这里单独将需要加的两个注解拿出来说:

  • @WebFilter(urlPatterns):这一注解添加在实现了Filter接口的过滤器实体类上,即定义过滤器的代码上,配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )。
  • @ServletComponentScan:这一注解加在后端服务器的启动类上,作用是开启SpringBoot项目对Servlet组件的支持。

这里提供一个登录功能的令牌校验过滤器代码:

/**
* 令牌校验过滤器
*/
@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;
        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);
            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);
    }

}

2.3 过滤器流程

image.png

单个过滤器

image.png

多个过滤器

当过滤器个数超过一个时,会形成过滤器链,过滤器的顺序按照类名以String类的升序排序,从小到大执行,执行到了最后一个过滤器放行之后,才会访问对应的web资源。

image.png

3.拦截器Interceptor

3.1 对比介绍拦截器

这里直接用过滤器与拦截器的对比来介绍拦截器 image.png

核心概念与定位

image.png

技术实现差异

3.2 自定义拦截器

@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true:放行 返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("在Handler拦截之前");
        return true;
    }

    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("在Handler执行之后,渲染视图执行之前");
    }

    //视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("渲染视图执行之后,释放资源");
    }
}

由于现在前后端分离 ,所以渲染视图的前后操作基本用不到,最重要的是preHanle方法的重写

3.3 注册配置拦截器

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

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

其中excludePathPatterns是遇到该路径直接放行,addPathPatterns是需要拦截的路径

同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;
    }

}

这里给出一个拦截器设置拦截路径的设置:

image.png

以上就是对Jwt令牌,Filter过滤器和Interceptor拦截器的介绍