过滤器 + 签名拦截器 + 签名工具类

Posted 364.99°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了过滤器 + 签名拦截器 + 签名工具类相关的知识,希望对你有一定的参考价值。

1. 拦截器和过滤器的概述

过滤器与拦截器的执行流程:

  • 过滤器implements Filter
    • 作用:
      • 拦截配置好的客户端请求,然后对Request和Response进行处理(对字符编码、跨域等问题进行过滤)
      • 随着web应用的启动而启动,只初始化一次
    • 方法:
      • init 容器启动时调用初始化方法,只会初始化一次
      • doFilter 每次请求都会调用doFilter方法,通过FilterChain 调用后续的方法
      • destroy 当容器销毁时,执行destory方法,只会被调用一次
  • 拦截器implements HandlerInterceptor
    • 作用
      • 对请求进行拦截,比如说验证请求的登录账号和密码,或者验证请求参数签名等
    • 方法:
      • preHandle 请求方法前置拦截,当注册的所有拦截器的 preHandle 执行完毕之后, DispatcherServlet 会根据返回结果来分配具体的 hanler 处理请求

      • postHandle preHandle 返回结果为true时,在Controller方法执行之后,视图渲染之前被调用

      • afterCompletionpreHandle 返回ture,并且整个请求结束之后,执行该方法

    • 注册
      • 编写一个类 implements WebMvcConfigurer
      • addInterceptors 中注册
        • addPathPatterns 需要拦截的请求
        • excludePathPatterns 不需要拦截的请求

2. 存储HttpServletRequest的输入流

因为 HttpServletRequest的输入流只能读取一次 , 而工程中会有多个地方需要获取 HttpServletRequest 的信息,所以需要先存储 HttpServletRequest的输入流

1.http请求包装类

HttpServletRequestWrapper 一个http请求包装类。

  • 使用方法:
    • extends HttpServletRequestWrapper
    • 实例化一个容器来存储流数据(数组或集合)
    • 重写getInputStream() 读取请求正文
    • 重写getReader() 读取请求正文

代码:

package com.chenjy.transfer.common.conf;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-24 10:37
 */
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper 

    // 存储流的容器
    private byte[] requestBody =null;

    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public RequestWrapper(HttpServletRequest request) throws IOException 
        super(request);
        // 将流复制到字节数组 requestBody 中
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    

    /**
     * @Description 获取请求体
     * @author chenJY
     * @date 2022/10/24 14:21
     * @param request
     * @return String
    */
    public String getBodyString(final ServletRequest request) 
        try 
            return inputStream2String(request.getInputStream());
         catch (IOException e) 
            log.error("", e);
            throw new RuntimeException(e);
        
    


    /**
     * @Description 获取请求体
     * @author chenJY
     * @date 2022/10/24 14:23
     * @return String
    */
    public String getBodyString() 
        final InputStream inputStream = new ByteArrayInputStream(requestBody);

        return inputStream2String(inputStream);
    

    /**
     * @Description 读取inputStream数据,并转换为String
     * @author chenJY
     * @date 2022/10/24 14:24
     * @param inputStream
     * @return String
    */
    private String inputStream2String(InputStream inputStream) 
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;

        try 
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String line;
            while ((line = reader.readLine()) != null) 
                sb.append(line);
            
         catch (IOException e) 
            log.error("异常:", e);
            throw new RuntimeException(e);
         finally 
            if (reader != null) 
                try 
                    reader.close();
                 catch (IOException e) 
                    log.error("", e);
                
            
        

        return sb.toString();
    

    @Override
    public ServletInputStream getInputStream() throws IOException 

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() 

            @Override
            public int read() throws IOException 
                return byteArrayInputStream.read();
            

            @Override
            public boolean isFinished() 
                return false;
            

            @Override
            public boolean isReady() 
                return false;
            

            @Override
            public void setReadListener(ReadListener readListener) 
                log.info("保存输入流......");
            
        ;
    

    @Override
    public BufferedReader getReader() throws IOException 
        return new BufferedReader(new InputStreamReader(getInputStream()));
    



2.过滤器

除了要写一个包装器外,我们还需要在过滤器里将原生的 HttpServletRequest 对象替换成我们自定义的RequestWrapper对象。

package com.chenjy.transfer.common.filter;

import com.chenjy.transfer.common.conf.RequestWrapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-24 9:41
 */
@Slf4j
@WebFilter(urlPatterns = "/*")
public class RequestFilter implements Filter 

    @Override
    public void init(FilterConfig filterConfig) throws ServletException 
        log.info("过滤器初始化......");
    

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
        ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
        chain.doFilter(requestWrapper, response);
    

    @Override
    public void destroy() 
        log.info("过滤器销毁......");
    


3. 签名生成

1.常量类

package com.chenjy.transfer.constant;

/**
 * @Author: chenJY
 * @Description: 签名验证相关常量
 * @Date: 2022-10-24 11:18
 */
public interface MyConstant 
    // 签名的key(参数名)
    String SIGN_KEY = "MySignKey";
    // 签名的标志
    String SIGN_FLAG = "MySFlag";
    // 签名后缀内容
    String SIGN_STR = "zuilitiaodengkanjian_XQJ";

2.签名工具类

package com.chenjy.transfer.common.util;

import com.chenjy.transfer.constant.MyConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;

/**
 * @Author: chenJY
 * @Description: 签名工具类(生成签名、签名加密)
 * @Date: 2022-10-24 11:21
 */
@Slf4j
public class SignUtil 

    /**
     * @Description 签名生成逻辑 (请求发送方法):
     *                  - 获取参数key,排序key(防止因参数顺序问题而导致签名不一致)
     *                  - 拼接key和value,并在末尾拼接上后缀内容 SIGN_STR
     *                  - 对拼接好的字符串进行 md5 加密
     * @author chenJY
     * @date 2022/10/24 11:24
     * @param params
     * @return String
    */
    public static String createSign(Map<String, Object> params) throws Exception 
        log.info("开始拼接签名......");
        StringBuilder ketStr = new StringBuilder();
        Object[] keys = params.keySet().toArray();
        Arrays.sort(keys);
        for (Object key : keys) 
            String valueStr = "";
            Object value = params.get(key);
            if (value != "") 
                valueStr = String.valueOf(value);
            
            ketStr.append(key)
                    .append("=")
                    .append(valueStr)
                    .append("——");
        
        ketStr.append(MyConstant.SIGN_STR);
        log.info("待验签名串:" + ketStr);
        return md5(ketStr.toString());
    

    public static String md5(String str) throws Exception
        // 指定加密类型
        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
        // 将字节数组转换为表示每个字节的十六进制值的字符串
        return Hex.encodeHexString(messageDigest.digest(str.getBytes(StandardCharsets.UTF_8)));
    

4.签名拦截器

1.拦截器类

package com.chenjy.transfer.common.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.chenjy.transfer.common.conf.RequestWrapper;
import com.chenjy.transfer.common.util.SignUtil;
import com.chenjy.transfer.constant.MyConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-24 11:13
 */
@Slf4j
@Component
public class SignInterceptor implements HandlerInterceptor 
    /**
     * @Description 拦截器的前置处理器
     * @author chenJY
     * @date 2022/10/24 14:38
     * @param request
     * @param response
     * @param handler
     * @return boolean
    */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        String strParam = new RequestWrapper(request).getBodyString();
        Map<String, Object> paramsMap = str2Map(strParam);
        if (paramsMap.get(MyConstant.SIGN_FLAG) == null) 
            log.error("参数未签名");
            return false;
        
        if (!verifySign(paramsMap)) 
            log.error("签名错误");
            return false;
        

        log.info("签名验证通过");
        return true;
    

    /**
     * @Description 验证签名是否一致
     * @author chenJY
     * @date 2022/10/24 14:43
     * @param paramsMap
     * @return boolean
    */
    public boolean verifySign(Map<String, Object> paramsMap) throws Exception 
        Set pNames = paramsMap.keySet();
        Iterator it = pNames.iterator();
        Map<String, Object> newParams = new HashMap<>();
        String originSign = paramsMap.get(MyConstant.SIGN_KEY).toString();
        while (it.hasNext()) 
            String pName = (String) it.next();
            if (MyConstant.SIGN_KEY.equals(pName)) 
                continue;
            
            Object pValue = paramsMap.get(pName);
            newParams.put(pName, pValue);
        
        String sign = SignUtil.createSign(newParams);
        log.info("sign-result:" + sign);
        return sign.equalsIgnoreCase(originSign);
    

    /**
     * @Description json字符串转Map
     * @author chenJY
     * @date 2022/10/24 14:32
     * @param str
     * @return Map<Object>
    */
    public Map<String, Object> str2Map(String str) 
        /*先转换为JSON对象*/
        JSONObject jsonParam = JSONObject.parseObject(str);
        Map<String, Object> resMap = new HashMap<>();
        Iterator it = jsonParam.entrySet().iterator();
        while (it.hasNext()) 
            Map.Entry<String, Object> entry = (Map.Entry<String, Object>) it.next();
            resMap.put(entry.getKey(), entry.getValue());
        
        return resMap;
    

2.注册拦截器

实现WebMvcConfigurer接口中的addInterceptors方法把自定义的拦截器类添加进来

package com.chenjy.transfer.common.conf;

import com.chenjy.transfer.common.interceptor.SignInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-24 14:48
 */
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer 
    @Bean
    public HandlerInterceptor getSignInterceptor() 
        return new SignInterceptor();
    

    /**
     * @Description 注册拦截器,声明要拦截的url
     * @author chenJY
     * @date 2022/10/24 14:50
     * @param registry
    */
    @Override
    public void addInterceptors(InterceptorRegistry registry拦截器的运行流程分析(项目源码寻踪)

解决HttpServletRequest的输入流只能读取一次的问题(转)

代码签名中的时间戳有啥用?

解决HttpServletRequest的输入流只能读取一次的问题

解决HttpServletRequest的输入流只能读取一次的问题

解决HttpServletRequest的输入流只能读取一次的问题