springboot解决跨域问题并使用拦截器拦截指定请求参数

1 篇文章 0 订阅
1 篇文章 0 订阅

 

当我们在进行开发时,会时常遇到跨域的问题,并且会有这种情况,我用ajax发送一个post请求,以jso形式传递,后端去拿数据拿不到对应的body请求体,导致一些列的问题。

根本原因就是,W3C规范这样要求了!在跨域请求中,分为简单请求(get和部分post,post时content-type属于application/x-www-form-urlencoded,multipart/form-data,text/plain中的一种)和复杂请求。而复杂请求发出之前,就会出现一次options请求。

我们可以直接在拦截器去通过request.getInputStream过去对应的body体,但是这样做会有问题,在我们的controller用@requestbody接受参数会报:

异常摘要:I/O error while reading input message; nested exception is java.io.IOException: Stream closed

org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed

 具体原因是因为,取 body参数时把request.getInputStream()关闭了,导致后面doFilter到@requestBody的对象拿取不到,所以我们要做的就是这次请求中保存这个流,具体如下:

package com.cri.common.utils;
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;

/**
 *  Request请求参数获取处理类
 *  author:fx
 *  2019/03/18
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {


    private final byte[] body;

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String bodyString = HttpHelper.getBodyString(request);
        body = bodyString.getBytes(Charset.forName("UTF-8"));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

以上我们是从request获取流保存起来,我们通过一个工具类去获取,如下:

package com.cri.common.utils;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

/**
 * Created by fx on 2019/3/19.
 */
public class HttpHelper {
    public static String getBodyString(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(
                    new InputStreamReader(inputStream, Charset.forName("UTF-8")));

            char[] bodyCharBuffer = new char[1024];
            int len = 0;
            while ((len = reader.read(bodyCharBuffer)) != -1) {
                sb.append(new String(bodyCharBuffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

此时我们需要定义一个过滤器,帮助我们进行request的传递:

package com.cri.common.config;

import com.cri.common.utils.BodyReaderHttpServletRequestWrapper;
import org.apache.commons.httpclient.HttpStatus;
import org.springframework.stereotype.Component;

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

/**
 * Created by fx on 2019/3/18.
 */
@Component
@WebFilter(filterName = "cookieFilter",urlPatterns = {"/**"})
public class CookieFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }



    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        HttpServletResponse resp = (HttpServletResponse) response;
        HttpServletRequest servletRequest =(HttpServletRequest)request;
        resp.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        resp.setHeader("Access-Control-Max-Age", "3600"); //设置过期时间
        resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, client_id, uuid, Authorization");
        resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // 支持HTTP 1.1.
        resp.setHeader("Pragma", "no-cache"); // 支持HTTP 1.0. response.setHeader("Expires", "0");
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            // 遇到post方法才对request进行包装
            String methodType = httpRequest.getMethod();
            // 上传文件时同样不进行包装
            String servletPath = httpRequest.getRequestURI().toString();
            if ("POST".equalsIgnoreCase(methodType)) {
                requestWrapper = new BodyReaderHttpServletRequestWrapper(
                        (HttpServletRequest) request);
            }
        }
        if (null == requestWrapper) {
            filterChain.doFilter(request, response);
        } else {
            filterChain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {

    }
}

这时的过滤器是不会被注入的,要在启动类里加上 @ServletComponentScan(basePackages = "com.cri.common.config.CookieFilter")此注解,后面对应的basePackages是类的包地址及类名。这样过滤器就会生效。可以看到过滤器中我们对post方法是进行了封装,将他的requset转换成我们的新的类BodyReaderHttpServletRequestWrapper,保存其中的流信息,并以ServletRequest继续传递下去,下一层我们的拦截器就会拿出body的具体参数进行拦截了,具体实现如下:

package com.cri.common.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cri.common.utils.BodyReaderHttpServletRequestWrapper;
import com.cri.common.utils.HttpHelper;
import com.cri.common.vo.AjaxResult;
import com.cri.service.CacheService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * Created by fx on 2019/3/14.
 *  * 拦截器
 * @ 用来判断用户的
 *1. 当preHandle方法返回false时,从当前拦截器往回执行所有拦截器的afterCompletion方法,再退出拦截器链。也就是说,请求不继续往下传了,直接沿着来的链往回跑。
 * 2.当preHandle方法全为true时,执行下一个拦截器,直到所有拦截器执行完。再运行被拦截的Controller。然后进入拦截器链
 */
public class CookieInterceptor implements HandlerInterceptor{
    @Autowired
    private CacheService cacheService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("---------------拦截器开始------------------");
        System.out.println("开始拦截瞎鸡儿发送接口的人");
        String tokenId = null;
        try{

            String requestMethod = request.getMethod();//请求方法
            if("options".equalsIgnoreCase(requestMethod)){
                System.out.println("option请求放开!");
                return true;
            }
            //获取请求参数
            String body = HttpHelper.getBodyString(request);
            JSONObject parameterMap = JSON.parseObject(body);
            tokenId = (String) parameterMap.get("tokenId");
            if(StringUtils.isNotEmpty(tokenId)){
                if (cacheService == null) {
                    BeanFactory beanFactory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
                    cacheService = (CacheService) beanFactory.getBean("cacheService");
                }
                String tokenValue = "";
               Object o = cacheService.get(tokenId);
                if (o == null) {
                    printJson(response,AjaxResult.NO_AUTHORIZED,"token过期,请重新登录");
                    return false;
                }else{
                    tokenValue = o.toString();
                }
               if(StringUtils.isNotEmpty(tokenValue)&&tokenValue.equals(tokenId)){
                   System.out.println("好吧,你通过了!");
                   return true;
               }else{
                   printJson(response,AjaxResult.NO_AUTHORIZED,"token过期,请重新登录");
                   return  false;
               }
            }else{
                printJson(response,AjaxResult.NO_AUTHORIZED,"token过期,请重新登录");
                return false;
            }
        }catch (Exception e){
            e.printStackTrace();
            printJson(response,AjaxResult.NO_AUTHORIZED,"token过期,请重新登录");
            return false;
        }
    }

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

    }

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

    }

    private static void printJson(HttpServletResponse response, String code,String message) {
        System.out.println("不好意思,你被拦住了兄弟!");
        AjaxResult ajaxResult = new AjaxResult();
        ajaxResult.setCode(code);
        ajaxResult.setMessage(message);
        String content = JSON.toJSONString(ajaxResult);
        printContent(response, content);
    }


    private static void printContent(HttpServletResponse response, String content) {
        PrintWriter pw = null;
        try {
            response.setContentType("application/json");
            response.setHeader("Content-type", "application/json;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            pw = response.getWriter();
            pw.write(content);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (pw != null)
                pw.close();
        }
    }
}

上面是拦截器的具体实现,下面我们要将这个类的具体实例放入spring容器中,那么会有人问,为什么要放进去,其实是因为我们在CookieInterceptor家了@autowird,引入了spring中的其他bean,那么我们自己也要在容器中,这样才能拿到对应的实例,要不然是取不到的,并且我们得让这个实例是项目启动的时候就加载的,这样他从一开始就构造自己的实例并且包含了@autowird引用的实例,才不会因为每次使用都要new而报空指针:

package com.cri.common.config;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

/**
 * Created by fx on 2019/3/14.
 */
@Configuration
public class InterceptorCofig extends WebMvcConfigurationSupport{

    @Bean
    public CookieInterceptor cookieInterceptor(){
        //这里对拦截器进行bean处理
        return new CookieInterceptor();
    }


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //cookieInterceptor()拦截器注册
        //        addPathPatterns添加需要拦截的命名空间;
        //        excludePathPatterns添加排除拦截命名空间
        registry.addInterceptor(cookieInterceptor()).addPathPatterns("/action/**").excludePathPatterns("/action/verify/sendCode")
        .excludePathPatterns("/action/login").excludePathPatterns("/action/comment/list").excludePathPatterns("/action/setting/**")
                .excludePathPatterns("/action/user/**");
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

    //跨域问题解决
    @Override
    protected void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/action/**");
    }
}

这样我们的普通请求拦截算是大功告成了。那么当然是没问题的,不过这里要讲的是我在CookieInterceptor实现里为什么要加这样一句话:

            String requestMethod = request.getMethod();//请求方法
            if("options".equalsIgnoreCase(requestMethod)){
                System.out.println("option请求放开!");
                return true;
            }

我们也注意到了,普通请求被我特别标注了,那肯定有不是普通的请求了,就是我已开始提到的:W3C规范这样要求了!在跨域请求中,分为简单请求(get和部分post,post时content-type属于application/x-www-form-urlencoded,multipart/form-data,text/plain中的一种)和复杂请求。而复杂请求发出之前,就会出现一次options请求。复杂请求我举个最平常的例子,ajax发送post请求,传递形式为json,如果不加上面的这句话 你会发现,你的请求一直会被拦截,因为你根本拿不到body,这是为什么呢?

就是以为不是普通请求的话,前台跨域post请求,由于CORS(cross origin resource share)规范的存在,浏览器会首先发送一次options嗅探,同时header带上origin,判断是否有跨域请求权限,服务器响应access control allow origin的值,供浏览器与origin匹配,如果匹配则正式发送post请求。什么是options请求呢?它是一种探测性的请求,通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。

在ajax中出现options请求,也是一种提前探测的情况,ajax跨域请求时,如果请求的是json,就属于复杂请求,因此需要提前发出一次options请求,用以检查请求是否是可靠安全的,如果options获得的回应是拒绝性质的,比如404\403\500等http状态,就会停止post、put等请求的发出。

因为我们的过滤器第一次收到只是option的预请求,根本没有带body内容,他只是一个确认,那么我们就将这种的请求方法放开,让他确认正常返回,这样他才会正常的走第二次post请求,问题就这么解决了~不过这个问题真的解决了好久,也找了各种博客,但是太杂乱无章了,所以自己总结下来,送给有缘人。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值