java 过滤器 + 拦截器 + JWT 原理以及实践

一、前言

还记得上次我写过几篇在实际项目中如何使用jwt《公众号授权 + jwt》、《小程序授权+ jwt》、《微信支付》

紧接着,就有个小伙伴,问了我一个这样的问题:授权使用=jwt签发token后,登录、注册等,用户是不需要token的,此时,应该怎么排除这些请求的url呢?
java 过滤器 + 拦截器 + JWT 原理以及实践_第1张图片

得嘞,今天咱们就掰扯掰扯这件事,如何使用“过滤器”或“拦截器”实现登录、注册的过滤

二、 是不是有人也这么写过?

在写此文时,我曾想到小编曾经的SAO操作,回看自己的代码,我滑稽的笑了。
java 过滤器 + 拦截器 + JWT 原理以及实践_第2张图片

因为小编的Controller层代码都是类似这样写的:

@RestController
public class LoginController {

    @Resource
    private HttpServletRequest request;
    
    @Resource
    private JwtUtil	jwtUtil;

    @ApiOperation(value = "根据订单id删除订单接口")
    @PostMapping(value = "/deleteById")
    public RespResult deleteById(String id) {
        String header = request.getHeader("token");//获取头信息,头中key为token
        if (StringUtils.isEmpty(header)) {
            return new RespResult(400, "请求头不能为空"); //头为空返回权限不足
        }
        if (!header.startsWith("Bearer ")) { // 和前端约定,头的值是Bearer + 空格 + jwt签发的token
            return new RespResult(400, "token错误");//头不符合约定,返回权限不足
        }
        String token = header.substring(7);  //提取头中的token
        Claims claims = jwtUtil.parseJWT(token); //使用jwt工具解密token
        if (claims == null) {
            return new RespResult(400, "token验证失败");
        }
        //调用service层删除订单
        // ...... 此处省略
        return new RespResult(200, "删除成功");
    }
}

卧CAO!小编这代码可真“丧心病狂”,现在只有一个接口,当接口有几十个时,写几十遍吗?,代码不冗余吗?何况一天就8小时,7小时在复制粘贴,怕是非常酸爽按。。。

接下来,我们就好好唠一唠:如何用过滤器或拦截器干掉上面冗余的代码!

三、有过滤器为啥不用

首先,我们要使用过滤器,肯定的先了解过滤器的原理;

过滤器(Filter)

过滤器 (Filter) 是处于客户端服务器资源文件之间的一道过滤网,在访问资源文件之前,通过一系列的过滤器 (也就是过滤链) 对请求进行修改、判断等,把不符合规则的请求在中途拦截或修改。也可以对响应进行过滤,拦截或修改响应。
java 过滤器 + 拦截器 + JWT 原理以及实践_第3张图片
如图,浏览器发出的请求先递交给第一个filter进行过滤,符合规则则放行,递交给filter链中的下一个过滤器进行过滤。过滤器在链中的顺序与它在yml或config中配置顺序有关。

大家都知道,filter中最重要的一个方法就是:doFilter()方法

在doFilter()方法中,chain.doFilter()前的一般是对request执行的过滤操作,chain.doFilter后面的代码一般是对response执行的操作。

来来来,我们看一下过滤链代码的执行顺序是怎么样的:
java 过滤器 + 拦截器 + JWT 原理以及实践_第4张图片
这图 so easy 吧:
chain.doFilter()前,对请求资源进行过滤(eg: 登录权限验证、资源访问权限控制、敏感词汇过滤、字符编码转换等等操作)
在chain.doFilter()后,给前端一些响应,例:{“code”: “6000”,“message”: “TOKEN 验证失败”}

来,咋们用filter,优化下小编的SAO代码:

首先,在yml配置文件中,设置不需要过滤的URI

filter:
  config:
    excludeUrls: /user/login,/user/register  #登录和注册不需过滤,直接放行

接下来,写一个过滤器:MyFilter

@Slf4j
@Order(1) //数字越小,越先执行此过滤器
@Component
@WebFilter(filterName = "MyFilter", urlPatterns = {"/**"}) // 过滤规则——所有
public class MyFilter implements Filter {

    @Resource
    private JwtUtils jwtUtils; // 自己jwt工具类

    @Value("${filter.config.excludeUrls}")
    private String excludeUrls; // 获取配置文件中不需要过滤的uri

    private List<String> excludes;

    // token验证失败时的响应消息
    private static final String VALID_ERROR = "{\"code\": \"6000\",\"message\": \"TOKEN 验证失败\"}";

    @Override
    public void init(FilterConfig filterConfig) {
    	// 初始化
    	// 移除配置文件中不过滤url,字符串的前空白和尾部空白
        excludes = Splitter.on(",").trimResults().splitToList(this.excludeUrls); 
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        String uri = request.getRequestURI(); //获取请求uri
        String token = request.getHeader("token"); // 获取头中token
        try {
            if (this.isExcludesUrl(uri)) { // 判断请求uri是否需要过滤(方法在下面)
                chain.doFilter(req, resp); // 不需要,放行
            } else {
                if (!validateParams(token)) { // 验证头中的token(方法在下面)
                    response.getWriter().write(VALID_ERROR); // 验证失败,返回验证失败消息
                    return;
                }
                chain.doFilter(request, resp); // 验证成功,放行
            }
        } catch (Exception ex) {
            log.error("Exception error", ex);
            response.getWriter().write(VALID_ERROR); // 抛出异常,返回验证失败消息
        } finally {
            response.flushBuffer(); // 将缓冲信息输出到页面
        }

    }

    private boolean validateParams(String token) {
    	// 验证是否为空,格式是否满足预定要求
        if (!StringUtils.isEmpty(token) && token.startsWith("bearer ")) {
            String token = token.substring(7);
            Map<String, Object> map = jwtUtils.extractJwt(token); // 掉用jwt解密方法解密
            if (map != null) { 
                return true;  // 解密成功,返回true
            }
        }
        return false; // 解密失败,返回false
    }
    
    private boolean isExcludesUrl(String path) {
        for (String v : this.excludes) { 
            if (path.startsWith(v)) {// 判断请求uri 是否满足配置文件uri要求
                return true;  // 满足、也就是请求uri 为 登录、注册,返回true
            }
        }
        return false; // 不满足、也就是请求uri 不是登录、注册,返回false
    }
}

最后,我们以空的请求头使用Postman访问下“根据订单id删除订单接口”接口:localhost:8080/deleteById;结果为:

{
    "code": 6000,
    "message": "TOKEN 验证失败"
}

这样,一个简单的filter,就取代了上面小编“辣眼睛”的操作,同时也排除了不需要验证的接口(登录、注册等),这才是真的SAO。。。

java 过滤器 + 拦截器 + JWT 原理以及实践_第5张图片

四、有拦截器为啥不用

来,我们先看看拦截器的原理;
java 过滤器 + 拦截器 + JWT 原理以及实践_第6张图片
拦截器,在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。拦截是AOP的一种实现策略

他有三个方法:

分别实现预处理后处理(调用了Service并返回ModelAndView,但未进行页面渲 染)、返回处理(已经渲染了页面)

  • preHandle中,可以进行编码、安全控制等在操作之前处理;
  • postHandle中,有机会修改ModelAndView在操作之后处理;
  • afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录,页面操作完之后。
来,咋们再用拦截器,优化下小编的SAO代码:

首先,编写拦截器配置文件类:

/**
 * Create By CodeCow on 2020/7/26.
 */
public class MyInterceptorConfig extends WebMvcConfigurationSupport {

    @Resource
    private MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,要声明拦截器对象和要拦截的请求
        registry.addInterceptor(myInterceptor)
                .addPathPatterns("/**") //所有路径都被拦截
                .excludePathPatterns("/user/login") // 排除用户登录请求
                .excludePathPatterns("/user/register"); // 排除用户注册请求
    }
}

接下来,编写拦截器实现类

@Component
public class MyInterceptor implements HandlerInterceptor {

    @Resource
    private JwtUtils jwtUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String headToken = request.getHeader("token"); // 获取头中token
        // 验证是否为空,格式是否满足预定要求
        if (!StringUtils.isEmpty(headToken) && headToken.startsWith("bearer ")) {
            String token = headToken.substring(7);
            Map<String, Object> map = jwtUtils.extractJwt(token); // 掉用jwt解密方法解密
            if (map != null) {
                request.setAttribute("claims_map", map); // 把jwt解密后的map放入request域中
                // 其他操作略。。。。。
            }
            // 其他操作略。。。。。
        }
        return true;
    }
}

最后,当我们需要用到token时,==直接从request域中获取“claims_map”==即可;

这样,一个简单的Interceptor,同样取代了上面小编的SAO操作,真香!

五、后记

好啦,今就先聊到这里吧,本文仅仅是抛砖引玉而已,浅聊了过滤器和拦截器配合jwt的简单实用,实际项目中如何选择,还得看个人爱好和编程风格

其次,大道万千,殊途同归,不管是过滤器还是拦截器,其实原理都大同小异,懂其原理,小小filter和Interceptor,还不信手拈来。。。

★★ 推荐 ★★

  • 答应我,别再if/else校验请求参数了可以吗

  • 微信小程序支付 + 公众号支付 (含源码)

  • 微信公众号授权+获取用户信息 + jwt登录 (含源码)

  • 微信小程序授权+获取用户手机号 + jwt登录 (含源码)

中国传统文化,先仔细看,若有用,再收藏, 给自己一点思考的时间,微信搜索:CodeCow,关注这个非常SAO的程序员

也可以加小编微信:CodeCow-6666 私信小编

你可能感兴趣的:(Java基础,java,spring,filter,过滤器,intellij,idea)