JAVA篇:Springboot 实现请求头&请求参数的组合验证

Springboot + 拦截器 + 过滤器 实现请求头&请求参数的组合验证

一、场景&需求:

    开发场景中涉及到请求头的一些值校验,例如经典的Authorization-token令牌鉴权登陆,这种基本借助拦截器就可以快速实现相关功能。
    但有些场景,不仅仅是对请求头进行校验,可能还需要对请求体中的参数做校验或处理,例如:请求数据中的敏感敏感数据脱敏。

二、解决方案 (大致2种):

1、直接controller层对参数进行相关处理/封装后,再往下传递来实现

优点:
      · 简单且可读性高;
缺点:
      · 修改工程量大,影响覆盖面广,假如该处理是针对全局来讲都需要,那么对应修改范围就是整个项目的controller层;
      · 扩展性差,假如后续针对该处理有了升级或变更,那么相关的操作又得重新刷一遍到项目中,显得整个动作十分繁杂且重复。
      · 涉及到需要请求头跟参数进行结合处理校验的场景,举个栗子:请求头Authorization-token可能并不能确定唯一性,需要与请求体中的字段userId来联合从而确定唯一,该情况需要在controller中额外添加了参数HttpservletRequest(工作量特别夯实)。

2、拦截器实现请求统一拦截

优点:
    · 影响代码量小,借助spring的aop思想不需要对业务代码做修改
    · 上述三种场景都可以满足,拦截器既获取servletRequest,可以截取请求头信息,也可以对参数进行截取
前提:
    · 对springboot的注解具备一定理解,熟悉拦截器和过滤器的使用,以及对servlet框架一定的理解(springboot的webmvc框架底层还是依赖servlet)

三、使用说明(以拦截器实现请求统一拦截) :

1、需要明白请求方式GET和POST分别用什么方式去提供参数,以及在拦截器中怎么获取到请求体参数:

1、请求方式为GET时,参数是通过放在url后面显性发送数据(query’param),在拦截器中可以通过 request.getParameter(arg) 方法来获取到这些请求参数
2、请求方式为POST时,参数是以表单(form-date)的形式隐性发送数据,在拦截器中可以通过 request.getInputStream() 方法来获取到这些请求参数,但是其是以流的形式存在,需要进行解析(框架servlet中,form表单的数据是以流的形式去存储)

2、需要了解以上俩种方式处理对本次请求后续的影响或弊端:

    ·针对GET请求中,拦截器的处理方式是没有问题的;
    ·针对POST请求,也就是第二种方式获取参数时,会存在一个问题:当在拦截器中通过 request.getInputStream() 获取了流之后,流程走到接口层会抛出如下异常:
(org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing)

3、POST请求处理方式出现异常的原因在于:

    之前也提到,在servlet中,form表单数据是通过流的方式存储,这个输入流类叫ServletInputStream(代码块1),通过代码块可以知道该类继承了 InputStream ,InputStream 类主要就是针对流提供了一套操作方法,其中readLine(xx)是对流进行读取。然后再去观察InputStream中的子类可以发现为了实现流能够重复读取,会用一个文本指针的变量(offset)用于记录当前流被读取的位置,而reset()方法就是专门把offset置为0,实现文本流重头开始读取。
    回到本文中的ServletInputStream中,可以发现该流仅仅只是重写了readLIne方法,并没有去重写reset()方法(InputStream的reset方法是直接抛出异常IOException),说明ServletInputStream的流只支持读取一次,在servlet中,ServletInputStream流是专门提供给接口层去获取参数的,当我们在拦截器中把这个流获取并解析了之后,后面的@RequestBody 就会因为该流的特性导致无法正常获取数据,从而抛出了上文所说的异常。

代码块1:ServletInputStream结构

public abstract class ServletInputStream extends InputStream {
    protected ServletInputStream() {
    }

    public int readLine(byte[] b, int off, int len) throws IOException {
        if (len <= 0) {
            return 0;
        } else {
            int count = 0;

            int c;
            while((c = this.read()) != -1) {
                b[off++] = (byte)c;
                ++count;
                if (c == 10 || count == len) {
                    break;
                }
            }

            return count > 0 ? count : -1;
        }
    }

    public abstract boolean isFinished();

    public abstract boolean isReady();

    public abstract void setReadListener(ReadListener var1);
四、案例代码(以拦截器实现请求统一拦截) :

1、借助过滤器(代码块2)对post请求的request进行拦截并二次封装

代码块2:Filter 结构

@WebFilter(urlPatterns = "/*",filterName = "authRequestFilter")
@Order(99)
public class AuthRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper =null;
        if (servletRequest instanceof HttpServletRequest){
            HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
            String method = httpServletRequest.getMethod();
            String contentType = httpServletRequest.getContentType();
            // post请求(多媒体表单例外)
            if (HttpMethod.POST.equals(method) && StringUtils.isNotEmpty(contentType) && !contentType.equals(MediaType.MULTIPART_FORM_DATA_VALUE)) {
                // 将httpServletRequest封装成 RequestWrapper(RequestWrapper里面主要就是针对表单数据流进行解析并改变 ServletInputStream 不能二次读取的特性)
                // 拦截器就可以直接通过body该属性去获取了,不需要解析流,避免后续的参数去获取流的时候出现异常 原先就可以对数据流表单进行解析,放到RequestWrapper.body属性中
                requestWrapper = new RequestWrapper(httpServletRequest);
            }
        }
        // 继续执行后续的过滤器链,将包装好的servletRequest往下传递
        // 过滤器链执行顺序在拦截器之前,因此可以实现偷梁换柱的效果
        if (Objects.isNull(requestWrapper)){
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }
}

2、过滤器对request只是做到拦截和封装,真正的数据流处理工作是需要 RequestWrapper 类(代码块3)来处理,RequestWrapper 类要处理的工作主要分为2点:

  • a.首先要对流进行解析,因此肯定要有需要实现流读写的方法,这是第一个工作
  • b.RequestWrapper 需要继承 HttpServletRequestWrapper,HttpServletRequestWrapper 可以理解为是将request的一些信息进行包装在一起,比如header头、cookie、session、ServletInputStream 等等),其中有2个方法 getInputStream()、getReader() 就是获取流和读取流的关键,而我们的第二步的任务是要为了让数据流能够二次读取

代码块3:RequestWrapper 结构

public class RequestWrapper extends HttpServletRequestWrapper {
    // 1MB
    private static final int BUFFER_SIZE =1024*1024*1;

    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                // 设置成1MB的缓冲字节数组,1char = 1byte
                char[] charBuffer = new char[BUFFER_SIZE];
                int bytesRead = -1;
                // 借助 bufferedReader 将数据从缓冲区中读到字节数组中,每次读取1MB位
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    // stringbuilder将char数组进行拼接,bytesRead最大到1MB
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {

        } finally {
            if (inputStream != null) {
                try {
                    // 流进行关闭,避免oom
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 将请求参数从流中转移到stringbuilder中,在通过tostring存到body变量
        // body变量设为final,保证只赋值一次
        body = stringBuilder.toString();
    }

    /**
     * 重写getInputStream方法就是用于给后续的servlet在@RequestBody获取参数值的时候,去调用获取流的时候从而调用该重写方法
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 因为之前的inputstream已经被关闭,这里通过已经存储下来的body重新写入到一个新的inputstream中
        // 这里采用 byteArrayInputStream 来替换原先的 ServletInputStream 
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
             /**
             * pos重置为0,又可以重复读取
             * @throws IOException
             */
            @Override
            public synchronized void reset() throws IOException {
                byteArrayInputStream.reset();
            }
        };
        return servletInputStream;

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8);
    }

    public String getBody() {
        return this.body;
    }

}

3、 最后一步,回到拦截器(代码块4)中对数据流的获取

    经过RequestWrapper 的将ServletInputStream 换为ByteArrayInputStream后,拦截器中就可以通过 request.getInputStream() 实现对数据进行解析,而解析完之后呢,需要调用 ServletInputStream.reset()方法 将读写的指针重置为0,这样就可以让后续的请求方法再次读取数据了,表单数据流不能二次读写的问题就解决了
    但是,聪明的小伙们们不知道有没有发现或思考,因为既然RequestWrapper 已经对流做了一次解析了,再将流转为byte最后封装进ByteArrayInputStream中,那为何不把这个已经解析出来的数据进行缓存呢,然后再拦截器中直接使用,这样就避免了拦截器再次去读取流的内存损耗了

@Component
public class AuthRequestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // instanceof 判断实例是否属于该类或继承至该类,也就是可以是自身或父类,但不能是子类,虽然编译能通过,但是实际运行的时候会抛异常
        if (!(handler instanceof HandlerMethod) && request instanceof RequestWrapper) {
            // 必须是RequestWrapper实例,否则是无法往下走的,因为过滤器会把符合场景的request进行封装成 RequestWrapper,如果不是该实例说明场景很明显不符合
            return true;
        }
        // 需要验证的请求头场景xxx
        if (StringUtils.isEmpty(auth) || Objects.isNull(authorizationUserClass)) {
            // 请求头有,但参数不是继承该类,说明没有userId关键字段,直接异常
            throw new UnAuthorizationException("假装验证不通过,抛出异常");
        }
        if (HttpMethod.GET.equals(request.getMethod()) && StringUtils.isEmpty(userId = request.getParameter("xxx"))) {
            throw new UnAuthorizationException("假装验证不通过,抛出异常");
        }
       //  (RequestWrapper) request).getBody() 直接获取已解析好的数据缓存直接使用,避免拦截器二次解析流带来的损耗
        Map map = JSONObject.parseObject(((RequestWrapper) request).getBody(), Map.class);
        if (HttpMethod.POST.equals(request.getMethod()) && !Objects.isNull(map.get("xxx"))) {
            throw new UnAuthorizationException("假装验证不通过,抛出异常");
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

最后以上的这些都是我结合框架的个人理解,如有出入,烦请告知,感激不尽!

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现自定义请求头的鉴权,可以通过在 WebSocket 握手过程中进行处理来实现。 具体步骤如下: 1. 自定义一个 `HandshakeInterceptor` 类,重写 `beforeHandshake()` 方法,在该方法中获取并验证请求头的鉴权信息。 例如: ```java public class AuthHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request.getHeaders().containsKey("Authorization")) { String token = request.getHeaders().getFirst("Authorization"); // 进行鉴权验证 if (isValidToken(token)) { // 鉴权成功,将鉴权信息存储到 attributes 中 attributes.put("token", token); return true; } } return false; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // do nothing } private boolean isValidToken(String token) { // 鉴权验证逻辑 return true; } } ``` 2. 在 WebSocket 配置类中注册该拦截器。 例如: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private AuthHandshakeInterceptor authHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler(), "/my-websocket") .addInterceptors(authHandshakeInterceptor) .setAllowedOrigins("*"); } @Bean public WebSocketHandler myWebSocketHandler() { return new MyWebSocketHandler(); } } ``` 在以上示例中,我们将自定义的 `AuthHandshakeInterceptor` 注册到了 WebSocket 配置类中,并在 `registerWebSocketHandlers()` 方法中通过 `addInterceptors()` 方法添加了该拦截器。 这样,在客户端连接 WebSocket 时,WebSocket 握手过程中会执行 `beforeHandshake()` 方法,进行请求头的鉴权验证。如果鉴权失败,则连接会被拒绝;如果鉴权成功,则将鉴权信息存储到 `attributes` 中,供后续使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值