在Web开发中,我们经常会遇到跨域的问题。跨域是指浏览器不能访问不同源(域名、协议或端口)的资源,这是浏览器为了安全而实施的同源策略。但是有时候我们确实需要访问其他源的资源,比如调用第三方的API或服务。这时候,我们就需要使用一些方法来解决跨域的问题。
CORS(Cross-Origin Resource Sharing,跨源资源共享)和代理(Proxy)是两种常用的解决跨域问题的方法。本文将介绍它们的原理和实现,并给出一些基于Spring Boot框架的示例代码。
CORS
CORS是一种基于HTTP头的机制,它允许服务器声明哪些源可以访问它的资源。浏览器在发送跨域请求时,会根据服务器返回的HTTP头来判断是否允许该请求。如果服务器允许,浏览器就可以正常访问该资源;如果服务器拒绝,浏览器就会抛出一个错误,并拦截该请求。
CORS涉及到以下几种HTTP头:
- Origin:请求头,表示请求来源的源(域名、协议和端口)。
- Access-Control-Allow-Origin:响应头,表示允许访问的源,可以是具体的源,也可以是*表示任意源。
- Access-Control-Request-Method:预检请求头,表示实际请求将使用的方法。
- Access-Control-Allow-Methods:预检响应头,表示允许访问的方法。
- Access-Control-Request-Headers:预检请求头,表示实际请求将携带的自定义头。
- Access-Control-Allow-Headers:预检响应头,表示允许携带的自定义头。
- Access-Control-Allow-Credentials:响应头,表示是否允许携带身份凭证(如Cookie或HTTP认证)。
- Access-Control-Max-Age:预检响应头,表示预检请求的结果可以缓存多久。
根据请求是否需要发送预检请求(OPTIONS方法),CORS分为简单请求和非简单请求两种。
简单请求满足以下条件:
- 使用GET、HEAD或POST方法之一。
- 除了浏览器自动设置的头外,只能携带以下安全的头:Accept、Accept-Language、Content-Language、Content-Type(只能是text/plain、multipart/form-data或application/x-www-form-urlencoded之一)。
- 不使用ReadableStream对象作为请求体。
对于简单请求,浏览器直接发送实际请求,并在响应中检查Access-Control-Allow-Origin头是否匹配当前源。如果匹配,浏览器就可以获取该资源;如果不匹配,浏览器就会抛出一个错误,并拦截该请求。
非简单请求不满足以上条件。对于非简单请求,浏览器先发送一个预检请求(OPTIONS方法),询问服务器是否允许该跨域请求。服务器需要返回所有相关的CORS头来回应预检请求。如果服务器同意该跨域请求,浏览器才会发送实际请求;如果服务器拒绝该跨域请求,浏览器就会抛出一个错误,并拦截该请求。
Spring Boot中实现CORS
在Spring Boot中,有多种方式来实现CORS:
- 在Controller类或方法上使用@CrossOrigin注解,来指定允许访问的源、方法、头等。
- 在WebMvcConfigurer接口中重写addCorsMappings方法,来全局配置CORS。
- 在Filter中自定义CORS过滤器,来手动设置响应头。
下面给出一些示例代码:
使用@CrossOrigin注解
假设我们有一个UserController类,提供了一个获取所有用户信息的接口。我们想要允许任意源访问该接口,并且允许携带身份凭证。我们可以在该类或方法上使用@CrossOrigin注解,并设置相应的属性:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@CrossOrigin(origins = "*", allowCredentials = "true") // 允许任意源,并且允许携带身份凭证
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/api/users")
public List<User> findAll() {
return userService.findAll();
}
}
使用WebMvcConfigurer接口
假设我们想要全局配置CORS,允许所有的接口都可以被任意源访问,并且允许携带身份凭证。我们可以创建一个配置类,实现WebMvcConfigurer接口,并重写addCorsMappings方法:
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有的接口
.allowedOrigins("*") // 允许任意源
.allowedMethods("*") // 允许任意方法
.allowedHeaders("*") // 允许任意头
.allowCredentials(true) // 允许携带身份凭证
.maxAge(3600); // 预检请求的缓存时间
}
}
使用Filter过滤器
假设我们想要自定义CORS过滤器,来手动设置响应头。我们可以创建一个过滤器类,实现Filter接口,并重写doFilter方法:
package com.example.demo.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 获取请求头中的Origin字段
String origin = httpServletRequest.getHeader("Origin");
if (origin != null) {
// 设置响应头中的Access-Control-Allow-Origin字段为请求头中的Origin字段
httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
// 设置响应头中的Access-Control-Allow-Credentials字段为true,表示允许携带身份凭证
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 设置响应头中的Access-Control-Allow-Methods字段为*,表示允许任意方法
httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
// 设置响应头中的Access-Control-Allow-Headers字段为*,表示允许任意头
httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");
// 设置响应头中的Access-Control-Max-Age字段为3600,表示预检请求的缓存时间为3600秒
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
}
chain.doFilter(request, response);
}
}
代理
代理是一种中间层,它可以在客户端和服务器之间转发请求和响应。通过使用代理,客户端可以绕过浏览器的同源策略,访问任意服务器的资源。
代理有多种类型,如正向代理、反向代理、透明代理等。本文只介绍一种简单的正向代理,即客户端知道代理的存在,并主动通过代理来访问服务器。
Spring Boot中实现代理
在Spring Boot中,有多种方式来实现代理:
- 使用RestTemplate类来发送HTTP请求,并转发响应。
- 使用HttpClient类来创建HTTP客户端,并转发响应。
- 使用OkHttp类来创建HTTP客户端,并转发响应。
下面给出一些示例代码:
使用RestTemplate类
假设我们有一个ProxyController类,提供了一个代理接口。我们想要通过该接口来访问其他服务器的资源。我们可以使用RestTemplate类来发送HTTP请求,并转发响应:
package com.example.demo.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ProxyController {
@GetMapping("/api/proxy")
public ResponseEntity<String> proxy() {
// 创建RestTemplate对象
RestTemplate restTemplate = new RestTemplate();
// 发送GET请求到目标服务器的接口,获取响应
ResponseEntity<String> response = restTemplate.getForEntity("http://target.com/api/resource", String.class);
// 转发响应到客户端
return response;
}
}
使用HttpClient类
假设我们有一个ProxyController类,提供了一个代理接口。我们想要通过该接口来访问其他服务器的资源。我们可以使用HttpClient类来创建HTTP客户端,并转发响应:
package com.example.demo.controller;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProxyController {
@GetMapping("/api/proxy")
public String proxy() throws Exception {
// 创建HttpClient对象
HttpClient httpClient = HttpClients.createDefault();
// 创建HttpGet对象,设置请求地址
HttpGet httpGet = new HttpGet("http://target.com/api/resource");
// 发送GET请求到目标服务器,获取响应
HttpResponse response = httpClient.execute(httpGet);
// 获取响应体内容
String content = EntityUtils.toString(response.getEntity());
// 转发响应体内容到客户端
return content;
}
}
使用OkHttp类
假设我们有一个ProxyController类,提供了一个代理接口。我们想要通过该接口来访问其他服务器的资源。我们可以使用OkHttp类来创建HTTP客户端,并转发响应:
package com.example.demo.controller;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProxyController {
@GetMapping("/api/proxy")
public String proxy() throws Exception {
// 创建OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
// 创建Request对象,设置请求地址
Request request = new Request.Builder()
.url("http://target.com/api/resource")
.build();
// 发送GET请求到目标服务器,获取响应
Response response = okHttpClient.newCall(request).execute();
// 获取响应体内容
String content = response.body().string();
// 转发响应体内容到客户端
return content;
}
}
总结
CORS和代理都是解决跨域问题的常用方法,但是它们也有各自的优缺点。CORS是一种更简单和安全的方法,但是它需要服务器端的支持和配置。代理是一种更灵活和强大的方法,但是它需要额外的网络开销和中间层的维护。