笔记来自学习慕课网课程:官网链接
编写前后端测试代码
源码:
ajaxclient:https://github.com/PengHongfu/ajaxclient
ajaxserver:https://github.com/PengHongfu/ajaxserver
后端springboot项目-ajaxserver
依赖文件
pom.xml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.peng</groupId>
<artifactId>ajaxserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>ajaxserver</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
ResulstBean.java
public class ResulstBean {
private String data;
public ResulstBean(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
TestController .java
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/get1")
public ResulstBean get1(){
System.out.println("get1");
return new ResulstBean("get1 ok");
}
}
localhost:8080/test/get1
前端springboot项目-ajaxclient
pom.xml
内容与后端项目一致
application.yml
server:
port: 8081
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<a href="#" onclick="get1()">发送get1请求</a>
<script>
function get1() {
$.getJSON("http://localhost:8080/test/get1").then(
function (value) {
console.info(value);
}
);
}
</script>
</body>
</html>
Failed to load http://localhost:8080/test/get1: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:8081’ is therefore not allowed access.
由8080
端口请求8081
端口,出现跨域错误
##引入Jasmine测试框架
在index.html
中引入依赖,编写测试用例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcss.com/jasmine/3.1.0/jasmine.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jasmine/3.1.0/jasmine.js"></script>
<script src="https://cdn.bootcss.com/jasmine/3.1.0/jasmine-html.js"></script>
<script src="https://cdn.bootcss.com/jasmine/3.1.0/boot.js"></script>
</head>
<body>
<!--<a href="#" onclick="get1()">发送get1请求</a>-->
<script>
/* function get1() {
$.getJSON("http://localhost:8080/test/get1").then(
function (value) {
console.info(value);
}
);
}*/
//测试用例超时时间
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
//请求接口的前缀
var base = "http://localhost:8080/test";
//测试模块
describe("ajax的测试用例",function () {
//测试方法
it("get1请求",function (done) {
//返回结果
var result;
$.getJSON(base+"/get1").then(function (value) {
result = value;
});
//由于是异步请求,需要使用setTimeout来校验
setTimeout(function () {
expect(result).toEqual({
"data":"get1 ok"
});
//校验完成,通知jasmine框架
done();
},100);
});
});
</script>
</body>
</html>
产生跨域问题的原因
- 浏览器原因
- 跨域
- XHR(XMLHttpRequest)请求
三个条件同时满足就会产生跨域问题
http://localhost:8080/test/get1 请求执行成功,后台打印了日志,
Response
成功返回了结果,说明后台处理没有问题,只是浏览器进行了拦截,而浏览器拦截了测试中的跨域XHR
请求。
在index.html
文件中请求非XHR
类型请求,浏览器不会拦截
<img src="http://localhost:8080/test/get1"/>
解决思路
- 浏览器
解决浏览器限制,客户端都要做改动,可行性不高
JSONP
动态创建一个
Script
请求,弊端只能发送GET
请求
- 跨域
针对调用方,
A域名
调用B域名
,通过代理(nginx/apache
),从浏览器发出去的请求都是A域名
的请求,在代理里面,把制定的URL路径
转到B域名
,在浏览器中,始终使用的都是A域名
,同一域名,从而避免跨域问题。这是一种隐藏(避免)跨域的思路。针对被调用方,使被调用方支持跨域,支持
HTTP
协议关于跨域方面的要求。例如:A域名
调用B域名
,返回时,B域名
请求返回时,加入返回字段要求浏览器允许A域名
调用。这是一种支持跨域的思路。
JSONP
Jsonp(JSON with Padding)
是 json
的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。
为什么我们从不同的域(网站)访问数据需要一个特殊的技术(
JSONP
)呢?这是因为同源策略。
同源策略,它是由Netscape
提出的一个著名的安全策略,现在所有支持JavaScript
的浏览器都会使用这个策略。
使用JSONP
之后,后台是否需要修改?
在
index.html
中,新增测试js
代码
//测试方法
it("JSONP请求",function (done) {
//返回结果
var result;
$.ajax({
url:base+"/get1",
dataType:"jsonp",
jsonp:"callback",
success:function (json) {
result = json;
}
});
//由于是异步请求,需要使用setTimeout来校验
setTimeout(function () {
expect(result).toEqual({
"data":"get1 ok"
});
//校验完成,通知jasmine框架
done();
},100);
});
出现异常,说明后台需要处理JSONP
请求
Spring
中的处理,新建类做切面处理。
JsonAdvice.java
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
/**
* Spring切面处理
* Created by PengHongfu 2018-06-22 16:02
*/
@ControllerAdvice
public class JsonAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonAdvice() {
super("callback");
}
}
JSONP
请求:
Request URL
http://localhost:8080/test/get1?callback=jQuery33109454435204550393_1529981307727&_=1529981307728
参数
_
为一个随机值,防止该请求被缓存。
Response
/**/jQuery33109454435204550393_1529981307727({"data":"get1 ok"});
普通的
ajax
请求返回的是json
格式数据,JOSNP
返回的是js
脚本;JSONP
请求参数中有callback
参数,JsonAdvice
类中约定了callback
参数,会对返回结果做相应处理,返回js
代码,其中callback
的值作为函数名,值为返回的JSON对象
。
JSONP的总结
JSONP
是一种非官方协议,是一种约定,约定了请求参数中如果包含指定的参数(默认为callback
),则服务器则将返回内容格式从json
改为js
代码(函数调用的形式),,其中返回的函数名为callback
的值,函数的参数为返回的JSON
对象。
弊端:
- 服务器需要改动代码支持
- 只支持GET请求方式
被调用方解决-支持跨域
http
服务器 应用服务器
Filter解决方案
后台先执行请求还是Filter判断?
服务端实现
在启动类中创建一个Filter
的Bean
AjaxserverApplication.java
...
@Bean
public FilterRegistrationBean registrationBean(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.addUrlPatterns("/*");
bean.setFilter(new CrosFilter());
return bean;
}
CrosFilter.java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by PengHongfu 2018-06-22 16:25
*/
public class CrosFilter implements javax.servlet.Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) servletResponse;
res.addHeader("Access-Control-Allow-Origin","http://localhost:8081");
res.addHeader("Access-Control-Allow-Methods","*");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
Access-Control-Allow-Methods:*
Access-Control-Allow-Origin:*
可以使用*,表示匹配所以的域和方法,但并不能匹配所以情况,跨域传递cookie的情况不适用。
简单请求和非简单请求
简单请求 ,会后台先执行请求,后判断
public class User {
private String name;
}
TestController.java
...
@PostMapping("/postJson")
public ResulstBean postJSon(@RequestBody User user){
System.out.println("postJson");
return new ResulstBean("postJson "+user.getName());
}
非简单请求的预检命令
Failed to load http://localhost:8080/test/postJson: Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
Request Headers
中询问了是否允许content-type
的请求头,需要在Response-Headers
中声明
res.addHeader("Access-Control-Allow-Headers","Content-Type");
//res.addHeader("Access-Control-Max-Age","3600");//预检命令缓存
带Cookie的跨域
TestController.java
...
@GetMapping("/getCookie")
public ResulstBean getCookie(@CookieValue(value = "cookie1") String cookie1){
System.out.println("getCookie");
return new ResulstBean("getCookie "+cookie1);
}
index.html
...
it("getCookie请求",function (done) {
//返回结果
var result;
$.ajax({
type:"get",
url: base+"/getCookie",
xhrFields:{
withCredentials:true
},
success:function (json) {
result = json;
}
});
//由于是异步请求,需要使用setTimeout来校验
setTimeout(function () {
expect(result).toEqual({
"data":"getCookie peng"
});
//校验完成,通知jasmine框架
done();
},100);
});
cookie
加在被调用方
执行
传递
Cookie
,Access-Control-Allow-Origin
不能使用通配符“*”,需使用具体路径。
Failed to load http://localhost:8080/test/getCookie: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’. Origin ‘http://localhost:8081’ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
//* 带cookie时,origin必须全匹配,不能使用* 并且要使用Allow-Credentials 为true
res.addHeader("Access-Control-Allow-Origin","http://localhost:8081");
//enable cookie
res.addHeader("Access-Control-Allow-Credentials","true");
对于Access-Control-Allow-Origin
这个Response-Headers
,在使用Cookie
的情况下,同时匹配多个域名的跨域请求,该如何处理?
从请求头中获取
Origin
信息,填充到Response-Headers
中
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) servletResponse;
HttpServletRequest req = (HttpServletRequest) servletRequest;
String origin = req.getHeader("Origin");
//支持所有跨域地址
if(!StringUtils.isEmpty(origin)){
res.addHeader("Access-Control-Allow-Origin",origin);
}
res.addHeader("Access-Control-Allow-Methods","*");
res.addHeader("Access-Control-Allow-Headers","Content-Type");
res.addHeader("Access-Control-Max-Age","3600");//预检命令缓存
//enable cookie
res.addHeader("Access-Control-Allow-Credentials","true");
filterChain.doFilter(servletRequest,servletResponse);
}
带自定义头的跨域
it("getHeader请求",function (done) {
//返回结果
var result;
$.ajax({
type:"get",
url: base+"/getHeader",
headers:{
"x-header1":"AAA"
},
beforeSend:function(xhr){
xhr.setRequestHeader("x-header2","BBB")
},
success:function (json) {
result = json;
}
});
//由于是异步请求,需要使用setTimeout来校验
setTimeout(function () {
expect(result).toEqual({
"data":"getHeader AAA BBB"
});
//校验完成,通知jasmine框架
done();
},100);
});
@GetMapping("/getHeader")
public ResulstBean getHeader(@RequestHeader(value = "x-header1") String header1,
@RequestHeader(value = "x-header2") String header2){
System.out.println("getHeader");
return new ResulstBean("getHeader "+header1 +" "+header2);
}
//res.addHeader("Access-Control-Allow-Headers","Content-Type,x-header1,x-header2");
约定大于配置
String headers = req.getHeader("Access-Control-Request-Headers");
//支持所有跨域请求头
if(!StringUtils.isEmpty(headers)){
res.addHeader("Access-Control-Allow-Headers",headers);
}
上面的请求例子都是客户端直接请求被调用方的应用服务器。
###被调用方,HTTP服务器(Nginx)解决
虚拟主机:多个域名指向同一个服务器,服务器根据不同域名把请求转到不同的应用服务器,看上去有多个主机,实际上只有一个主机。
配置host
C:\Windows\System32\drivers\etc\hosts
127.0.0.1 b.com
在nginx conf
目录下新建vhost
目录
在nginx.conf
文件下添加
include vhost/*.conf;
在vhost
目录下新建b.com.conf
文件
b.com.conf
server{
listen 80;
server_name b.com;
location /{
proxy_pass http://localhost:8080;
}
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Max-Age 3600;
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Allow-Origin $http_origin;
if ($request_method = OPTIONS){
return 200;
}
}
屏蔽后端使用过滤器
修改请求地址
启动nginx
nginx.exe -s reload
重新加载配置
被调用方-Spring框架解决方案
在Controller
类中或方法上添加注解@CrossOrigin
@RestController
@RequestMapping("/test")
@CrossOrigin
public class TestController {
}
调用方解决跨域-隐藏跨域
反向代理:以代理服务器来接受
internet
上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
访问同一个域名的两个不同URL
,访问不同的两个应用服务器。
在 C:\Windows\System32\drivers\etc\hosts
文件中新增host
a.com表示调用方的虚拟主机
127.0.0.1 b.com a.com
新建b.com.conf
server{
listen 80;
server_name a.com;
location /{
proxy_pass http://localhost:8081;
}
location /ajaxServer{
proxy_pass http://localhost:8080/test;
}
}
var base = "/ajaxServer";
启动nginx
cookie
报错,添加cookie