JavaScript 九种跨域方式实现原理


Posted in Javascript onFebruary 11, 2019

前言

前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

一、什么是跨域?

1.什么是同源策略及其限制内容?

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。

JavaScript 九种跨域方式实现原理

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

2.常见跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

JavaScript 九种跨域方式实现原理

特别说明两点:

第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。

第二:在跨域问题上,仅仅是通过“URL 的首部”来识别而不会根据域名对应的 IP 地址是否相同来判断。“URL 的首部”可以理解为“协议, 域名和端口必须匹配”。

这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

二、跨域解决方案

1.jsonp

1)JSONP 原理

利用 script 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP 请求一定需要对方的服务器做支持才可以。

2)JSONP 和 AJAX 对比

JSONP 和 AJAX 相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但 AJAX 属于同源策略,JSONP 属于非同源策略(跨域请求)

3)JSONP 优缺点

JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击。

4)JSONP 的实现流程

  • 声明一个回调函数,其函数名(如 show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的 data)。
  • 创建一个script标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是 show,它准备好的数据是show('我不爱你')。
  • 最后服务器把准备的数据通过 HTTP 协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP 函数。

// index.html
function jsonp({ url, params, callback }) {
 return new Promise((resolve, reject) ={
 let script = document.createElement('script')
 window[callback] = function(data) {
  resolve(data)
  document.body.removeChild(script)
 }
 params = { ...params, callback } // wd=b&callback=show
 let arrs = []
 for (let key in params) {
  arrs.push(`${key}=${params[key]}`)
 }
 script.src = `${url}?${arrs.join('&')}`
 document.body.appendChild(script)
 })
}
jsonp({
 url: 'http://localhost:3000/say',
 params: { wd: 'Iloveyou' },
 callback: 'show'
}).then(data ={
 console.log(data)
})

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行 show()这个函数,打印出'我不爱你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
 let { wd, callback } = req.query
 console.log(wd) // Iloveyou
 console.log(callback) // show
 res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

5) jQuery 的 jsonp 形式

JSONP 都是 GET 和异步请求的,不存在其他的请求方式和同步请求,且 jQuery 默认就会给 JSONP 的请求清除缓存。

$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});

2.cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

1) 简单请求

只要同时满足以下两大条件,就属于简单请求

条件 1:使用下列方法之一:

  • GET
  • HEAD
  • POST

条件 2:Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

2) 复杂请求

不符合以上条件的请求就肯定是复杂请求了。

复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
 res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
 console.log(req.headers)
 res.end('我不爱你')
})

接下来我们看下一个完整复杂请求的例子,并且介绍下 CORS 请求相关的字段

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
 if (xhr.readyState === 4) {
 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
  console.log(xhr.response)
  //得到响应头,后台需设置Access-Control-Expose-Headers
  console.log(xhr.getResponseHeader('name'))
 }
 }
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
 let origin = req.headers.origin
 if (whitList.includes(origin)) {
 // 设置哪个源可以访问我
 res.setHeader('Access-Control-Allow-Origin', origin)
 // 允许携带哪个头访问我
 res.setHeader('Access-Control-Allow-Headers', 'name')
 // 允许哪个方法访问我
 res.setHeader('Access-Control-Allow-Methods', 'PUT')
 // 允许携带cookie
 res.setHeader('Access-Control-Allow-Credentials', true)
 // 预检的存活时间
 res.setHeader('Access-Control-Max-Age', 6)
 // 允许返回的头
 res.setHeader('Access-Control-Expose-Headers', 'name')
 if (req.method === 'OPTIONS') {
  res.end() // OPTIONS请求不做任何处理
 }
 }
 next()
})
app.put('/getData', function(req, res) {
 console.log(req.headers)
 res.setHeader('name', 'jw') //返回一个响应头,后台需设置
 res.end('我不爱你')
})
app.get('/getData', function(req, res) {
 console.log(req.headers)
 res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

上述代码由http://localhost:3000/index.html向http://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。

3.postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的 iframe 消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • message: 将要发送到其他 window 的数据。
  • targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。

// a.html
 <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe//等它加载完触发一个事件
 //内嵌在http://localhost:3000/a.html
 <script>
  function load() {
  let frame = document.getElementById('frame')
  frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
  window.onmessage = function(e) { //接受返回数据
   console.log(e.data) //我不爱你
  }
  }
 </script>
// b.html
 window.onmessage = function(e) {
 console.log(e.data) //我爱你
 e.source.postMessage('我不爱你', e.origin)
 }

4.websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

原生 WebSocket API 使用起来不太方便,我们使用Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。

我们先来看个例子:本地文件 socket.html 向localhost:3000发生数据和接受数据

// socket.html
<script>
 let socket = new WebSocket('ws://localhost:3000');
 socket.onopen = function () {
  socket.send('我爱你');//向服务器发送数据
 }
 socket.onmessage = function (e) {
  console.log(e.data);//接收服务器返回的数据
 }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
 ws.on('message', function (data) {
 console.log(data);
 ws.send('我不爱你')
 });
})

5. Node 中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:

  • 接受客户端请求 。
  • 将请求 转发给服务器。
  • 拿到服务器 响应 数据。
  • 将 响应 转发给客户端。

JavaScript 九种跨域方式实现原理

我们先来看个例子:本地文件 index.html 文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据。

// index.html(http://127.0.0.1:5500)
 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
 <script>
  $.ajax({
  url: 'http://localhost:3000',
  type: 'post',
  data: { name: 'xiamen', password: '123456' },
  contentType: 'application/json;charset=utf-8',
  success: function(result) {
   console.log(result) // {"title":"fontend","password":"123456"}
  },
  error: function(msg) {
   console.log(msg)
  }
  })
  </script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) ={
 // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
 response.writeHead(200, {
 'Access-Control-Allow-Origin': '*',
 'Access-Control-Allow-Methods': '*',
 'Access-Control-Allow-Headers': 'Content-Type'
 })
 // 第二步:将请求转发给服务器
 const proxyRequest = http
 .request(
  {
  host: '127.0.0.1',
  port: 4000,
  url: '/',
  method: request.method,
  headers: request.headers
  },
  serverResponse ={
  // 第三步:收到服务器的响应
  var body = ''
  serverResponse.on('data', chunk ={
   body += chunk
  })
  serverResponse.on('end', () ={
   console.log('The data is ' + body)
   // 第四步:将响应结果转发给浏览器
   response.end(body)
  })
  }
 )
 .end()
})
server.listen(3000, () ={
 console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) ={
 if (request.url === '/') {
 response.end(JSON.stringify(data))
 }
})
server.listen(4000, () ={
 console.log('The server is running at http://localhost:4000')
})

上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在 index.html 文件打印出{"title":"fontend","password":"123456"}

6.nginx 反向代理

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。

先下载nginx,然后将 nginx 目录下的 nginx.conf 修改如下:

// proxy服务器
server {
 listen  80;
 server_name www.domain1.com;
 location / {
  proxy_pass http://www.domain2.com:8080; #反向代理
  proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
  index index.html index.htm;

  # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
  add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
  add_header Access-Control-Allow-Credentials true;
 }
}

最后通过命令行nginx -s reload启动 nginx

// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
 var params = qs.parse(req.url.substring(2));
 // 向前台写cookie
 res.writeHead(200, {
  'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
 });
 res.write(JSON.stringify(params));
 res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

7.window.name + iframe

window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

其中 a.html 和 b.html 是同域的,都是http://localhost:3000;而 c.html 是http://localhost:4000

// a.html(http://localhost:3000/b.html)
 <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
 <script>
 let first = true
 // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
 function load() {
  if(first){
  // 第1次onload(跨域页)成功后,切换到同域代理页面
  let iframe = document.getElementById('iframe');
  iframe.src = 'http://localhost:3000/b.html';
  first = false;
  }else{
  // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
  console.log(iframe.contentWindow.name);
  }
 }
 </script>

b.html 为中间代理页,与 a.html 同域,内容为空。

// c.html(http://localhost:4000/c.html)
 <script>
 window.name = '我不爱你'
 </script>

总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

8.location.hash + iframe

实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。

同样的,a.html 和 b.html 是同域的,都是http://localhost:3000;而 c.html 是http://localhost:4000

// a.html
 <iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
 <script>
 window.onhashchange = function () { //检测hash的变化
  console.log(location.hash);
 }
 </script>
// b.html
 <script>
 window.parent.parent.location.hash = location.hash
 //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
 </script>
 // c.html
 console.log(location.hash);
 let iframe = document.createElement('iframe');
 iframe.src = 'http://localhost:3000/b.html#idontloveyou';
 document.body.appendChild(iframe);
9.document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中 a 的值

// a.html
<body>
 helloa
 <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
 <script>
 document.domain = 'zf1.cn'
 function load() {
  console.log(frame.contentWindow.a);
 }
 </script>
</body>
// b.html
<body>
 hellob
 <script>
  document.domain = 'zf1.cn'
  var a = 100;
 </script>
</body>

三、总结

CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案

JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

不管是 Node 中间件代理还是 nginx 反向代理,主要是通过同源策略对服务器不加限制。

日常工作中,用得比较多的跨域方案是 cors 和 nginx 反向代理

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery中 noConflict() 方法使用
Apr 25 Javascript
jQuery中scrollLeft()方法用法实例
Jan 16 Javascript
JS中闭包的经典用法小结(2则示例)
Dec 28 Javascript
Bootstrap导航条学习使用(二)
Feb 08 Javascript
微信小程序 仿猫眼实现实例代码
Mar 14 Javascript
Vue Spa切换页面时更改标题的实例代码
Jul 15 Javascript
详解webpack模块化管理和打包工具
Apr 21 Javascript
详解vue+webpack+express中间件接口使用
Jul 17 Javascript
Vue利用History记录上一页面的数据方法实例
Nov 02 Javascript
vue插件draggable实现拖拽移动图片顺序
Dec 01 Javascript
详细分析vue响应式原理
Jun 22 Javascript
JavaScript实现淘宝商品图切换效果
Apr 29 Javascript
ES6 迭代器与可迭代对象的实现
Feb 11 #Javascript
详解mpvue中小程序自定义导航组件开发指南
Feb 11 #Javascript
如何用JavaScript实现功能齐全的单链表详解
Feb 11 #Javascript
vue2.0实现的tab标签切换效果(内容可自定义)示例
Feb 11 #Javascript
vue实现动态显示与隐藏底部导航的方法分析
Feb 11 #Javascript
Vue指令v-for遍历输出JavaScript数组及json对象的常见方式小结
Feb 11 #Javascript
react配置antd按需加载的使用
Feb 11 #Javascript
You might like
DC动画电影《黑暗正义联盟》曝预告 5月5日上线数字平台
2020/04/09 欧美动漫
AJAX的跨域访问-两种有效的解决方法介绍
2013/06/22 PHP
PHP微信红包API接口
2015/12/05 PHP
PHP递归算法的简单实例
2019/02/28 PHP
Thinkphp5 如何隐藏入口文件index.php(URL重写)
2019/10/16 PHP
Laravel 5.5 实现禁用用户注册示例
2019/10/24 PHP
创建一个复制UBB软件信息的链接或按钮的js代码
2008/01/06 Javascript
setTimeout 不断吐食CPU的问题分析
2009/04/01 Javascript
javascript 用记忆函数快速计算递归函数
2010/03/15 Javascript
js substr、substring和slice使用说明小记
2011/09/15 Javascript
js动态控制table的tr、td增加及删除的具体实现
2014/04/30 Javascript
js实现多选项切换导航菜单的方法
2015/02/06 Javascript
js上传图片及预览功能实例分析
2015/04/24 Javascript
JavaScript 基础函数_深入剖析变量和作用域
2016/05/18 Javascript
JavaScript知识点总结(十一)之js中的Object类详解
2016/05/31 Javascript
phantomjs导出html到pdf的方法总结
2017/10/19 Javascript
Vue2.0实现组件数据的双向绑定问题
2018/03/06 Javascript
jquery获取select选中值的文本,并赋值给另一个输入框的方法
2018/08/21 jQuery
使用weixin-java-tools完成微信授权登录、微信支付的示例
2018/09/26 Javascript
Python实现二叉树结构与进行二叉树遍历的方法详解
2016/05/24 Python
好用的Python编辑器WingIDE的使用经验总结
2016/08/31 Python
详解Python中的分组函数groupby和itertools)
2018/07/11 Python
Python检查和同步本地时间(北京时间)的实现方法
2018/12/03 Python
python实现海螺图片的方法示例
2019/05/12 Python
python中的句柄操作的方法示例
2019/06/20 Python
使用pyecharts生成Echarts网页的实例
2019/08/12 Python
Python编程快速上手——正则表达式查找功能案例分析
2020/02/28 Python
TensorFlow Autodiff自动微分详解
2020/07/06 Python
通过实例简单了解python yield使用方法
2020/08/06 Python
什么叫应用程序域?什么是受管制的代码?什么是强类型系统?什么是装箱和拆箱?
2016/08/13 面试题
中专毕业个人的自荐信格式
2013/09/21 职场文书
十八届三中全会感言
2014/03/10 职场文书
协会周年庆活动方案
2014/08/26 职场文书
青春雷锋观后感
2015/06/10 职场文书
单位提档介绍信
2015/10/22 职场文书
《卧薪尝胆》读后感3篇
2019/12/26 职场文书