前情提要
前后端进行项目的分离操作过程中,使用到了 Ajax 技术,因为后端采用了 serverless 的 python3 实现了部分接口,上文提供了一种通用的 JWT 验证方式,可以实现分布式系统下的多点鉴权认证功能,本文将会通过讲述跨域的原理进行分布式系统下前后端分离资源请求的一种文体,同源策略导致的跨域问题,应该如何解决。
跨域原理
因为浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
1 什么是跨域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
当前页面url | 被请求页面url | 是否跨域 | 原因 |
否 | 同源(协议、域名、端口号相同) | ||
跨域 | 协议不同(http/https) | ||
跨域 | 主域名不同(test/baidu) | ||
跨域 | 子域名不同(www/blog) | ||
跨域 | 端口号不同(8080/7001) |
2 非同源限制
【1】无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
【2】无法接触非同源网页的 DOM
【3】无法向非同源地址发送 AJAX 请求
3 跨域解决方案
【1】设置document.domain解决无法读取非同源网页的 Cookie问题
【2】跨文档通信 API:window.postMessage()
【3】JSONP
【4】CORS
【5】webpack本地代理
【6】websocket
【7】Nginx反向代理
说明: 上述方式均可以实现前后端的跨域访问问题,基于当下分布式技术的发展,代理或者配置 Nginx 的方式仅能作用于小规模或者单体的产品应用,符合产品云原生发展的时代大趋势的方面,建议通过 CORS 或者 JSONP的方式实现跨域访问的技术逻辑,其他的方式目前还是存在部分技术项目的架构中的,但是随着时代的洪流,总有一天会消逝不见的。
JSON & JSONP
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写。
JSONP实现跨域请求的原理简单的说,就是动态创建 script 标签,然后利用 script 的src 不受同源策略约束来跨域获取数据。
JSONP 由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的 JSON 数据。
动态创建 script 标签,设置其src,回调函数在src中设置:
JSONP目前还是比较流行的跨域方式,虽然JSONP使用起来方便,但是也存在一些问题:
首先, JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此在使用不是你自己运维的 Web 服务时,一定得保证它安全可靠。
其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script
元素新增了一个 onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。
1 jQuery封装JSONP
对于经常用jQuery的开发者来说,能注意到jQuery封装的.ajax中,但是其本质与.ajax实现跨域的代码参考如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>jQuery实现JSONP</title>
</head>
<body>
<div id="mydiv">
<button id="btn">点击</button>
</div>
</body>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<script type="text/javascript">
$(function(){
$("#btn").click(function(){
$.ajax({
async : true,
url : "https://api.douban.com/v2/book/search",
type : "GET",
dataType : "jsonp", // 返回的数据类型,设置为JSONP方式
jsonp : 'callback', //指定一个查询参数名称来覆盖默认的 jsonp 回调参数名 callback
jsonpCallback: 'handleResponse', //设置回调函数名
data : {
q : "javascript",
count : 1
},
success: function(response, status, xhr){
console.log('状态为:' + status + ',状态是:' + xhr.statusText);
console.log(response);
}
});
});
});
</script>
</html>
最后的结果与JavaScript通过动态添加 script 标签得到的结果是一样的。
通过$.getJSON()
利用getJSON来实现,只要在地址中加上callback=?参数即可,参考代码如下:
$.getJSON("https://api.douban.com/v2/book/search?q=javascript&count=1&callback=?", function(data){
console.log(data);
});
备注: 上述方式也能实现跨域的功能。
CORS 实现跨域
CORS:全称"跨域资源共享"(Cross-origin resource sharing)
CORS需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持CORS,IE则不能低于IE10。CORS的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送ajax请求并无差异。so,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信。
1 CORS 请求类型
CORS分为简单请求和非简单请求(需预检请求)两类
a. 符合以下条件的,为简单请求
请求方式使用下列方法之一:
GET
HEAD
POST
Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
对于简单请求,浏览器会直接发送CORS请求,具体说来就是在header中加入origin请求头字段。同样,在响应头中,返回服务器设置的相关CORS头部字段,Access-Control-Allow-Origin字段为允许跨域请求的源。请求时浏览器在请求头的Origin中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回.
b. 非简单请求(预检请求)
使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
Content-Type 的值不属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
当发生符合非简单请求(预检请求)的条件时,浏览器会自动先发送一个options请求,如果发现服务器支持该请求,则会将真正的请求发送到后端,反之,如果浏览器发现服务端并不支持该请求,则会在控制台抛出错误,如下:
如果非简单请求(预检请求)发送成功,则会在头部多返回以下字段:
Access-Control-Allow-Origin: http://localhost:3001 //该字段表明可供那个源跨域
Access-Control-Allow-Methods: GET, POST, PUT // 该字段表明服务端支持的请求方法
Access-Control-Allow-Headers: X-Custom-Header // 实际请求将携带的自定义请求首部字段
2 CORS字段介绍
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
使用CORS简单请求,非常容易,对于前端来说无需做任何配置,与发送普通ajax请求无异。唯一需要注意的是,需要携带cookie信息时,需要将withCredentials设置为true即可。CORS的配置,完全在后端设置,配置起来也比较容易,目前对于大部分浏览器兼容性也比较好。CORS优势也比较明显,可以实现任何类型的请求,相较于JSONP跨域只能使用get请求来说,也更加的便于我们使用。
CORS 前后端代码
前端:
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="application/x-javascript">
addEventListener("load", function() {
setTimeout(hideURLbar, 0);
}, false);
function hideURLbar() {
window.scrollTo(0, 1);
}
</script>
<meta name="keywords" content="Flat Dark Web Login Form Responsive Templates, Iphone Widget Template, Smartphone login forms,Login form, Widget Template, Responsive Templates, a Ipad 404 Templates, Flat Responsive Templates" />
<link rel="stylesheet" href="http://rainbow-river.vpc123.xyz:31112/function/main/static/css/style.css" type='text/css' />
<!-- <link rel="stylesheet" href="../public/css/style.css" type='text/css' /> -->
</head>
</head>
<script type="text/javascript" src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
function submit() {
var username = $('#user').val();
var password = $('#password').val();
var rePassword = $('#repassword').val();
if(password != rePassword) {
alert("两次密码不一致,请认真校验!")
}else{
var qdata = {
"user": $('#user').val(),
"pwd": $('#password').val(),
"pathPage": 'register',
};
$.ajax({
url: 'http://rainbow-river.vpc123.xyz:31112/function/mainback',
path: 'register',
dataType: 'text',
type: 'POST',
contentType: "application/json;charset=utf-8",
data: JSON.stringify(qdata),
success: function(data) {
// splitStr=data.substring(2,data.length)
// var jsonObj=JSON.parse(splitStr)
// console.log(jsonObj)
// console.log(jsonObj.data)
// console.log(jsonObj.data.status)
alert("账号注册成功")
// window.location.href='/function/main/login.html'
},
error: (error) => {
alert('账号注册失败')
console.log(error)
}
})
}
}
</script>
<body>
<!--SIGN UP-->
<h1>rainbow-river</h1>
<div class="login-form">
<div class="close"> </div>
<div class="head-info">
<label class="lbl-1"> </label>
<label class="lbl-2"> </label>
<label class="lbl-3"> </label>
</div>
<div class="clear"> </div>
<div class="avtar">
<img src="http://rainbow-river.vpc123.xyz:31112/function/main/static/images/avtar.png" />
<!-- <img src="../public/images/avtar.png" /> -->
</div>
<form>
<input type="text" name="user" id="user" class="text" value="Username" onfocus="this.value = '';" onblur="if (this.value == '') {this.value = 'Username';}">
<div class="key">
<input type="password" name="password" id="password" value="Password" onfocus="this.value = '';" onblur="if (this.value == '') {this.value = 'Password';}">
<input type="password" name="repassword" id="repassword" value="Password" onfocus="this.value = '';" onblur="if (this.value == '') {this.value = 'rePassword';}">
</div>
</form>
<div class="signin">
<input type="submit" value="Register" onclick="submit()">
</div>
</div>
</body>
</html>
后端代码:
from .pkg.db.mysql import mysql_client as mysqlClient
# from urllib.parse import parse_qs
from .pkg.jwt import pkg_jwt as jwtClient
from .pkg.consts import consts
import hashlib
import json,requests
myClient = mysqlClient.mysqlpython()
# 后端函数服务入口
def handle(reqStr):
"""handle a request to the function
Args:
req (str): request body
"""
req=json.loads(reqStr)
choice = req["pathPage"] # 获取选择
def register():
consts.resp["data"]=registerPage(req)
def login():
consts.resp["data"]=loginPage(req)
def user():
consts.resp["data"]=userPage(req)
def default(): # 默认情况下执行的函数
default
switch = {'register': register,
'login': login,
'user': user,
}
switch.get(choice, default)() # 执行对应的函数,如果没有就执行默认的函数
return consts.resp
# 后端函数服务001: 用户注册服务
def registerPage(req):
user=req["user"]
pwd=req["pwd"]
select = 'select * from user_user where name="'+user+'" limit 0,1;'
req=myClient.get(select)
for userData in req:
if userData['name']==user:
return {'status': 'false','msg':'the account already exists,please log in.'}
image_url = "https://i.postimg.cc/h4MZfC1Z/peng.jpg"
token = user + pwd
uuid = hashlib.md5(token.encode()).hexdigest()[:10]
hashPwd=hashlib.md5(pwd.encode()).hexdigest()
insertSql = 'insert into user_user (name, password,uuid,image_url) VALUES("'+user+'","'+hashPwd+'","'+uuid+'","'+image_url+'"); '
myClient.run(insertSql)
return {'status': 'true','msg':'registration success.'}
# 后端函数服务002: 用户登陆服务
def loginPage(req):
user=req["user"]
pwd=req["pwd"]
select = 'select * from user_user where name="'+user+'" limit 0,1;'
req=myClient.get(select)
for userData in req:
status = 'false'
token=''
hashPwd = hashlib.md5(pwd.encode()).hexdigest()
if userData['password']==hashPwd:
# 当登录校验通过,则为用户创建并返回token
token = jwtClient.create_token(user,consts.SECRET_KEY)
status='true'
return {'status': status, 'info': {'user':user,'token': token}}
# 后端函数服务003: 用户信息服务
def userPage(req):
user = req["user"]
token=req["token"]
if not token:
return {'status': 'false', 'msg': 'token不允许为空!'}
payload, msg = jwtClient.validate_token(token,consts.SECRET_KEY)
select = 'select * from user_user where name="'+user+'" limit 0,1;'
baseData=myClient.get(select)
return {'status': 'true', 'info': baseData,'payload':payload,'msg':msg}
def indexPage(req):
print('This is the indexPage')
def default():
print('No such case')
resp = {
"data": {},
"args": {},
"headers": {
'Access-Control-Allow-Origin':'*',
'Access-Control-Allow-Methods':'PUT,GET,POST,DELETE',
'Access-Control-Allow-Headers':'Referer,Accept,Origin,User-Agent',
},
}
总结
在国庆节,搞什么鬼?因为要实现 Serverless 的前后端分离,自己开始摸索各种乱七八糟的技术点细节问题,本来想自己生成 Token,结果呢,又要保存又要缓存,调研了挺久综合对比发现了 JWT 这种逆天的存在,处理了一个问题以后,然后开始跨域请求问题的处理,主要原因是因为前后端如果都要通过一致的源进行开发就太扯淡了。然后还有一个比赛的半决赛入围,所以才开始想要加快速度进行开发设计的,本来选择的 Openfaas 作为开发平台进行的设计,本来作为统一的前端使用 nodeJs 进行的设计实现,后端通过 Python3 写后端接口来着,但是因为后端的入参只接受 Text,所有的 Json 请求过去以后就全部成立为了空字符串,无法达到入参的目的,存在两个办法,自己改动 Openfaas 的模板或者项目代码,然后提交变更,这个思路想了下感觉不太现实,的虽然可以做到,但是本来的路就跑偏了,所以只能接受后端入参为文本类型,如果可以通过 JSON 的方式的话,其实最想通过 JSONP 的方式实现跨域的设计的,没得办法,所以只能设计成为 CORS 的方式了,其实也还行,关键就是这样的探索可以加快我的整体开发脚步了。