JS唬住面试官拿高薪 一 网络篇

笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。

第一章: 能不能说一说XSS攻击

什么是 XSS 攻击?

XSS 全称是 Cross Site Scripting(即跨站脚本),为了和 CSS 区分,故叫它XSS。XSS 攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作。
这些操作一般可以完成下面这些事情:

  • 窃取Cookie
  • 监听用户行为,比如输入账号密码后直接发送到黑客服务器
  • 修改 DOM 伪造登录表单
  • 在页面中生成浮窗广告

通常情况,XSS 攻击的实现有三种方式—存储型反射型文档型

存储型

存储型,顾名思义就是将恶意脚本存储了起来,确实,存储型的 XSS 将脚本存储到了服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果。
常见的场景是留言评论区提交一段脚本代码,如果前后端没有做好 转义 的工作,那评论内容存到了数据库,在页面渲染过程中直接执行, 相当于执行一段未知逻辑的 JS 代码,是非常恐怖的。这就是存储型的 XSS 攻击。

反射型

反射型XSS指的是恶意脚本作为网络请求的一部分。比如我输入:

http://www.kenguba.com?q=<script>alert("你完蛋了")</script>

这杨,在服务器端会拿到q参数,然后将内容返回给浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样就被攻击了。
之所以叫它反射型, 是因为恶意脚本是通过作为网络请求的参数,经过服务器,然后再反射到HTML文档中,执行解析。和存储型不一样的是,服务器并不会存储这些恶意脚本

文档型

文档型的 XSS 攻击并不会经过服务端,而是作为中间人的角色,在数据传输过程劫持到网络数据包,然后修改里面的 html 文档
这样的劫持方式包括 WIFI路由器劫持 或者 本地恶意软件 等。

防范措施

明白了三种XSS攻击的原理,我们能发现一个共同点: 都是让恶意脚本直接能在浏览器中执行。
那么要防范它,就是要避免这些脚本代码的执行。
为了完成这一点,必须做到一个信念,两个利用

一个信念

千万不要相信任何用户的输入!无论是在前端和服务端,都要对用户的输入进行转码或者过滤
如:

<script>alert('你完蛋了')</script>

转码后变为:

&lt;script&gt;alert(&#39;你完蛋了&#39;)&lt;/script&gt;

这样的代码在 html 解析的过程中是无法执行的。
当然也可以利用关键词过滤的方式,将 script 标签给删除

利用 CSP

CSP(Content-Security-Policy),即浏览器中的内容安全策略,它的核心思想就是服务器决定浏览器加载哪些资源,具体来说可以完成以下功能:

  • 限制其他域下的资源加载。
  • 禁止向其它域提交数据。
  • 提供上报机制,能帮助我们及时发现 XSS 攻击

你可以使用 Content-Security-PolicyHTTP头部 来指定你的策略,像这样:

Content-Security-Policy: policy

实例1: 一个网站管理者想要所有内容均来自站点的同一个源 (不包括其子域名)

Content-Security-Policy: default-src 'self'

实例2: 一个网站管理者允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)

Content-Security-Policy: default-src 'self' *.trusted.com

实例3: 一个网站管理者允许网页应用的用户在他们自己的内容中包含来自任何源的图片, 但是限制音频或视频需从信任的资源提供者(获得),所有脚本必须从特定主机服务器获取可信的代码.

Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

实例4: 在这里,各种内容默认仅允许从文档所在的源获取, 但存在如下例外:

  • 图片可以从任何地方加载(注意 “*” 通配符)。
  • 多媒体文件仅允许从 media1.com 和 media2.com 加载(不允许从这些站点的子域名)。
  • 可运行脚本仅允许来自于userscripts.example.com。

一个线上银行网站的管理者想要确保网站的所有内容都要通过SSL方式获取,以避免攻击者窃听用户发出的请求。

Content-Security-Policy: default-src https://onlinebanking.jumbobank.com

该服务器仅允许通过HTTPS方式并仅从onlinebanking.jumbobank.com域名来访问文档。
实例5: 一个在线邮箱的管理者想要允许在邮件里包含HTML,同样图片允许从任何地方加载,但不允许JavaScript或者其他潜在的危险内容(从任意位置加载)。

Content-Security-Policy: default-src 'self' *.mailsite.com; img-src *

注意这个示例并未指定script-src (en-US)。在此CSP示例中,站点通过default-src指令的对其进行配置,这也同样意味着脚本文件仅允许从原始服务器获取。

启用违例报告
默认情况下,违规报告并不会发送。为启用发送违规报告,你需要指定report-uri (en-US)策略指令,并提供至少一个URI地址去递交报告:

Content-Security-Policy: default-src 'self'; report-uri http://reportcollector.example.com/collector.cgi

然后你需要设置你的服务器能够接收报告,使其能够以你认为恰当的方式存储并处理这些报告。
作为报告的JSON对象报告包含了以下数据:
document-uri 发生违规的文档的URI。
referrer 违规发生处的文档引用(地址)。
blocked-uri 被CSP阻止的资源URI。如果被阻止的URI来自不同的源而非文档URI,那么被阻止的资源URI会被删减,仅保留协议,主机和端口号。
violated-directive 违反的策略名称。
original-policy 在Content-Security-PolicyHTTP 头部中指明的原始策略。

违例报告样本
我们假设页面位于http://example.com/signup.html。它使用如下策略,该策略禁止任何资源的加载,除了来自cdn.example.com的样式表。

Content-Security-Policy: default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports

signup.html的HTML像这样:

<!DOCTYPE html>
<html>
  <head>
    <title>Sign Up</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    ... Content ...
  </body>
</html>

你能看出其中错误吗?样式表仅允许加载自cdn.example.com,然而该页面企图从自己的源 (http://example.com)加载。当该文档被访问时,一个兼容CSP的浏览器将以POST请求的形式发送违规报告到http://example.com/_/csp-reports,内容如下:

{
  "csp-report": {
    "document-uri": "http://example.com/signup.html",
    "referrer": "",
    "blocked-uri": "http://example.com/css/style.css",
    "violated-directive": "style-src cdn.example.com",
    "original-policy": "default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports"
  }
}

如你所见,该报告在blocked-uri字段中包含了违规资源的完整路径 ,但情况并非总是如此。
比如,当signup.html试图从  http://anothercdn.example.com/stylesheet.css加载CSS时,浏览器将不会包含完整路径,而只会保留源路径 (http://anothercdn.example.com)。
CSP技术规范对此古怪行为给出了解释。大体上说,这样是为了防止泄露跨域资源的敏感信息。

利用 HttpOnly

很多 XSS 攻击脚本都是用来窃取Cookie, 而设置 Cookie 的 HttpOnly 属性后,JavaScript 便无法读取 Cookie 的值。这样也能很好的防范 XSS 攻击

第二章: 能不能说一说CSRF攻击?

什么是CSRF攻击?

CSRF(Cross-site request forgery), 即跨站请求伪造,指的是黑客诱导用户点击链接,打开黑客的网站,然后黑客利用用户目前的登录状态发起跨站请求
举个例子, 你在某个论坛点击了黑客精心挑选的小姐姐图片,你点击后,进入了一个新的页面。那么恭喜你,被攻击了:)
你可能会比较好奇,怎么突然就被攻击了呢?接下来我们就来拆解一下当你点击了链接之后,黑客在背后做了哪些事情。
可能会做三样事情。列举如下:

自动发 GET 请求

黑客网页里面可能有一段这样的代码:

<img src="https://xxx.com/info?user=hhh&count=100">

进入页面后自动发送 get 请求,值得注意的是,这个请求会自动带上关于 xxx.com 的 cookie 信息(这里是假定你已经在 xxx.com 中登录过)。
假如服务器端没有相应的验证机制,它可能认为发请求的是一个正常的用户,因为携带了相应的 cookie,然后进行相应的各种操作,可以是转账汇款以及其他的恶意操作。

自动发 POST 请求

黑客可能自己填了一个表单,写了一段自动提交的脚本。

<form id='hacker-form' action="https://xxx.com/info" method="POST">
  <input type="hidden" name="user" value="hhh" />
  <input type="hidden" name="count" value="100" />
</form>
<script>document.getElementById('hacker-form').submit();</script>

同样也会携带相应的用户 cookie 信息,让服务器误以为是一个正常的用户在操作,让各种恶意的操作变为可能。

诱导点击发送 GET 请求

在黑客的网站上,可能会放上一个链接,驱使你来点击:

<a href="https://xxx/info?user=hhh&count=100" taget="_blank">点击进入甜蜜恋爱世界</a>

点击后,自动发送 get 请求,接下来和自动发 GET 请求部分同理。
这就是CSRF攻击的原理。和XSS攻击对比,CSRF 攻击并不需要将恶意代码注入用户当前页面的html文档中,而是跳转到新的页面,利用服务器的 **验证漏洞 **和 **用户之前的登录状态 **来模拟用户进行操作。

防范措施

利用Cookie的SameSite属性

CSRF攻击中重要的一环就是自动发送目标站点下的 Cookie,然后就是这一份 Cookie 模拟了用户的身份。因此在Cookie上面下文章是防范的不二之选。
恰好,在 Cookie 当中有一个关键的字段,可以对请求中 Cookie 的携带作一些限制,这个字段就是SameSite。
SameSite可以设置为三个值,Strict、Lax和None。

  • Strict模式下 浏览器完全禁止第三方请求携带Cookie。比如请求baidu.com网站只能在baidu.com域名当中请求才能携带 Cookie,在其他网站请求都不能
  • Lax模式 就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能
  • None模式下 也就是默认模式,请求会自动携带上 Cookie
验证来源站点

这就需要要用到请求头中的两个字段: OriginReferer

  • Origin 只包含域名信息
  • Referer 包含了具体的 URL 路径

当然,这两者都是可以伪造的,通过 Ajax 中自定义请求头即可,安全性略差

使用验证码

关键操作页面加上验证码,后台收到请求后通过判断验证码可以防御CSRF。但这种方法对用户不太友好

在请求地址中添加token并验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有token或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于session之中,然后在每次请求时把token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成

http://url?csrftoken=tokenvalue

而对于 POST 请求来说,要在 form 的最后加上

 <input type="hidden" name="csrftoken" value="tokenvalue"/>

这样就把token以参数的形式加入请求了。

CSRF Token (在HTTP 头中自定义属性并验证)

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去
Django作为 Python 的一门后端框架,如果是用它开发过的同学就知道,在它的模板(template)中, 开发表单时,经常会附上这样一行代码:

{% csrf_token %}

这就是 CSRF Token 的典型应用。那它的原理是怎样的呢?
首先,浏览器向服务器发送请求时,服务器生成一个字符串,将其植入到返回的页面中。
然后浏览器如果要发送请求,就必须带上这个字符串,然后服务器来验证是否合法,如果不合法则不予响应。这个字符串也就是CSRF Token,通常第三方站点无法拿到这个 token, 因此也就是被服务器给拒绝

第三章: xhr、ajax、axios、featch的使用详解和区别

xhr

在Fetch出现之前我们发送异步请求默认都是通过ajax,底层使用了宿主环境的(XHR)XMLHTTPRequest 对象来实现异步请求。实现代码如下:

var xhr = new XHMHttpRequest()
xhr.open('GET', url, true) //第三个参数为true(异步执行,不等待后台返回)。而为false(同步执行,等待返回后再执行下一步)
xhr.responseType = 'json'
xhr.onload = function () { console.log(xhr.response) }
xhr.setRequestHeader(header, value);
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.stastus === 200) {
    console.log(xhr.responseText);
  }
}
xhr.onerror = function () { console.log() }
xhr.send()

为了方便,自己封装一个简易的ajax

<script>
	function $_ajax(request) {
		request.type = request.type.toUpperCase()
		var send_data = null, data_arr = []
		for (var key in request.data) { data_arr.push(key + '=' + request.data[key]) }
		var data_str = data_arr.join('&')
		var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
		if (request.type === 'GET') {
			request.url += '?' + data_str
		}
		xhr.open(request.type, request.url)
		if (request.type === 'POST') {
			xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
			send_data = data_str
		}
		xhr.send(send_data)  //参数的形式是:"key=value&id=123"

		xhr.addEventListener('readystatechange', function () {
			if (this.readyState !== 4) return
			try {
				request.done(JSON.parse(this.responseText))
			} catch (e) {
				request.done(this.responseText)
			}
		})
	}
</script>

<script>
	var sayHi = new Function("a", "b", "alert(a + b)")
	sayHi("Hello", "Word")
	$_ajax({
		type: 'get',
		url: 'jsonp.php',
		data: {
			username: "张三",
			password: '123456789'
		},
		done: function (data) {
			console.log(data)
		}
	})
	$_ajax({
		type: 'Post',
		url: 'jsonp.php',
		data: {
			username: "张三",
			password: '123456789'
		},
		done: function (data) {
			console.log(data)
		}
	})
</script>

使用 Promise 的形式来解决回调地狱

const getJSON = function (url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}

jsonp

JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;

const jsonp = ({ url, params, callbackName }) => {
  const generateUrl = () => {
    let dataSrc = ''
    for (let key in params) {
      if (params.hasOwnProperty(key)) {
        dataSrc += `${key}=${params[key]}&`
      }
    }
    dataSrc += `callback=${callbackName}`
    return `${url}?${dataSrc}`
  }
  return new Promise((resolve, reject) => {
    const scriptEle = document.createElement('script')
    scriptEle.src = generateUrl()
    document.body.appendChild(scriptEle)
    window[callbackName] = data => {
      resolve(data)
      document.removeChild(scriptEle)
    }
  })
}

ajax

当然我们一般会用一些封装过的ajax实现解决传统方式的繁琐写法以及xhr低版本浏览器可能出现的兼容问题,比如jquery的ajax

$.ajaxSettings.async = false  //设置同步
$.ajax({
  url: "/delete_book",                   // 请求的后端url
  type: "post",                          // 请求方式 get|post|put|delete
  dataType: "json",                      // 指明后端返回的数据格式
  data: JSON.stringify({ name: "张三" }), // 向后端发送的请求体数据
  contentType: "application/json",       // 指明向后端发送的数据格式
  headers: {                             //请求头
    "X-CSRFToken": getCookie("csrf_token")
  },
  success: function (res) {
    if (res.code == 0) {
      location.href = "/";
      location.reload()
    }
  }
})
$.ajaxSettings.async = true  //设置异步




//类似Promise写法
$.ajax({
  url: '/api/v1/users',
  type: 'post',
  data: JSON.stringify({ name: "张三" }), //转json数据
  dataType: 'json',
  contentType: 'application/json',
  headers: {
    "X-CSRFToken": getCookie("csrf_token") // 请求头,将csrf_token值放到请求中,方便后端csrf进行验证
  }
})
.then(function (res) { })



$.ajaxSettings.async = false  //设置同步
$.post('/cart/update', params).then(function (data) {
  if (data.res == 5) {
    total_count = data.total_count
    sussess_update = true
  }
  else {
    alert(data.errmsg)
    sussess_update = false
  }
  $.ajaxSettings.async = true  //设置异步
})
  • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
  • 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)

axios

是一个基于promise网络请求库,作用于node.js和浏览器中,它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)

  • 在服务端它使用原生node.js的http模块
  • 而在客户端 (浏览端) 则使用XMLHttpRequests
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})
  .then(function (response) {
    console.log(response)
  })
  .catch(function (error) {
    console.log(error)
  })
  • 从 node.js 创建 http 请求
  • 从浏览器中创建 XMLHttpRequest
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止CSRF
  • 提供了一些并发请求的接口(重要,方便了很多的操作)

fetch

符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
更好更方便的写法更加底层,提供的API丰富(request, response)脱离了XHR,是ES规范里新的实现方式

fetch(url, {
  body: JSON.stringify(data), // must match 'Content-Type' header
  cache: 'no-cache',          // *default, no-cache, reload, force-cache, only-if-cached
  credentials: 'same-origin', // include, same-origin, *omit
  headers: {
    'user-agent': 'Mozilla/4.0 MDN Example',
    'content-type': 'application/json'
  },
  method: 'POST',          // *GET, POST, PUT, DELETE, etc.
  mode: 'cors',            // no-cors, cors, *same-origin
  redirect: 'follow',      // manual, *follow, error
  referrer: 'no-referrer', // *client, no-referrer
})

  • fetch的语法简洁,更语义化
  • 基于promise,支持async/await
  • 同构方便,使用isomorphic-fetch
  • Fetch API更加现代
  • Fetch API更底层
  • Fetch API更接近未来
  • 缺点
  • fetch只针对网络错误报错,http状态码错误不报错,对400,500都当做成功的请求,需要封装去处理
  • fetch不支持abort,无法终止
  • fetch不支持超时控制,使用setTimeout和Promise.reject实现的超时控制不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有原生检测请求进度的方式,XHR可以
  • 默认情况下fetch不发送cookie,除非手动配置,
  • credentials: ‘include’ // 默认请求是否带上cookie
  • credentials: ‘same-origin’ //同源的情况下带cookie
  • credentials: ‘omit’ // 忽略cookie

fetch API更加现代
XHR 和 Fetch API 最显著的区别就是调用方式不同。这一点大家应该都知道吧。
举个例子,下面两端代码完成的是同一功能:

// 用 XHR 发起一个GET请求
var xhr = new XHMHttpRequest()
xhr.open('GET', url, true) //第三个参数为true(异步执行,不等待后台返回)。而为false(同步执行,等待返回后再执行下一步)
xhr.responseType = 'json'
xhr.onload = function () { console.log(xhr.response) }
xhr.setRequestHeader(header, value);
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.stastus === 200) {
    console.log(xhr.responseText);
  }
}
xhr.onerror = function () { console.log() }
xhr.send()

// 用 Fetch 完成同样的请求
fetch(url).then(function (response) {
  return response.json()
}).then(function (jsonData) {
  console.log(jsonData)
}).catch(function () {
  console.log()
})

fetch API更底层
其实,刚才说到的 Fetch API 并不是指仅仅一个 fetch 方法,还包括 Request、 Response、Headers、Body都一系列原生对象。对于传统的XHR而言,你必须使用它的一个实例来发出请求和处理响应。 但是通过Fetch API,我们还能够通过刚才提到的原生对象,明确的配置请求和响应。这些底层的抽象让 Fetch API 比 XHR 更灵活。
举个例子,现在要下载一个很大的 utf-8 格式的 txt 文件,我们通过流式的响应体而不是文本的形式读取,最后显示在一个div中

document.addEventListener('DOMContentLoaded', function (e) {
  var url = 'test.txt'
  var div = document.getElementById('content')

  var progress = 0
  var contentLength = 0

  fetch(url).then(function (response) {
    // 通过响应头获取文件大小
    contentLength = response.headers.get('Content-Length')

    var pump = function (reader) {
      return reader.read().then(function (result) {
        // 如果流中的内容读取完毕,result.done的值会变为true
        if (!result.done) {
          // 获取流中的数据
          var chunk = result.value

          var text = ''
          // 流中的数据是一串字节码,需要做转码
          for (var i = 3; i < chunk.byteLength; i++) {
            text += String.fromCharCode(chunk[i])
          }

          // 添加到页面的div中
          div.innerHTML += text

          // 还可以用流的长度显示当前进度
          progress += chunk.byteLength
          console.log(((progress / contentLength) * 100) + '%')

          // 开始读取下一个流
          return pump(reader)
        }
      })
    }

    // 开始读取流中的信息
    return pump(response.body.getReader())
  }).catch(function (error) {
      console.log(error)
    })
})

在上面的例子中,我们不止使用了流来下载文件,还通过响应头获取了响应的具体信息,显示了下载的进度。虽然使用XHR也能做到使用流来读取文件,不过现在应该只有IE浏览器支持。但是 Fetch API 提供了访问数据的实际字节的方法,而 XHR 的 responseText 只有文本形式,这意味着在某些场景下它的作用可能非常有限。

fetch API更接近未来
当我们在谈论 Fetch API 时,我们在谈论的不止是这些已经胜过 XHR 的地方,更是在谈论 Fetch API未来的可能性。比如未来基于 Fetch 和 Service Worker 的缓存和请求拦截技术。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值