当我们在进行开发时,会时常遇到跨域的问题,并且会有这种情况,我用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请求,问题就这么解决了~不过这个问题真的解决了好久,也找了各种博客,但是太杂乱无章了,所以自己总结下来,送给有缘人。