Web安全之 SQL 注入

Web安全之 SQL 注入

随着互联网的快速发展,网络安全问题日益受到人们的关注。 SQL 注入是一种常见的网络安全攻击方式,严重威胁着数据库的安全。本文将从 SQL 注入的概念、原理、危害以及防范措施等方面,对网络安全中的 SQL 注入进行深入探讨。

概念

SQL 注入(SQL Injection)是指攻击者在用户输入的数据中注入恶意的 SQL 语句,从而在未经授权的情况下实现对数据库的非法操作。当应用程序没有对用户输入的数据进行有效的验证和过滤时,攻击者可以利用这一漏洞,篡改原始的 SQL 语句,进而窃取、篡改或删除数据库中的数据。

原理

SQL 注入的原理主要基于应用程序对用户输入数据的处理不当。攻击者通过在用户输入的数据中插入恶意的 SQL 语句,使得应用程序在构建 SQL 查询时,将恶意语句作为查询的一部分发送给数据库执行。这样,攻击者就可以绕过应用程序的验证,直接对数据库进行操作。

危害

  1. 数据泄露:攻击者可以通过 SQL 注入获取数据库中的敏感信息,如用户密码、身份信息、银行账户等,从而导致数据泄露和用户隐私泄露。
  2. 数据篡改:攻击者可以利用 SQL 注入修改数据库中的数据,破坏数据的完整性和准确性。
  3. 数据删除:攻击者可以通过 SQL 注入删除数据库中的数据,导致数据丢失和业务受损。
  4. 服务器被控制:攻击者还可以通过 SQL 注入获取数据库的完全控制权限,进一步对服务器进行攻击和控制。

案例

下面是一个简单的 Node.jsExpress 框架中的 SQL 注入案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const express = require('express');
const mysql = require('mysql');
const app = express();

// 创建数据库连接
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test'
});

connection.connect();

// 定义一个简单的路由处理函数,存在 SQL 注入漏洞
app.get('/users', (req, res) => {
const username = req.query.username; // 从查询字符串获取用户名

// 直接将用户名插入 SQL 查询中,没有进行任何过滤或转义
const query = `SELECT * FROM users WHERE username = '${username}'`;

connection.query(query, (error, results, fields) => {
if (error) throw error;
res.send(results);
});
});

app.listen(3000, () => {
console.log('App listening on port 3000');
});

在上述代码中,我们定义了一个简单的 Express 应用,它监听 /users 路由,并从查询字符串中获取 username 参数。然后,我们直接将 username 插入 SQL 查询语句中。由于没有对 username 进行任何形式的过滤或转义,攻击者可以通过在查询字符串中插入恶意的 SQL 代码来操纵查询结果,甚至执行任意的 SQL 语句。

例如,攻击者可以通过以下 URL 尝试注入攻击:

1
http://localhost:3000/users?username='; DROP TABLE users; --

这个 URL 会导致 SQL 查询变成:

1
SELECT * FROM users WHERE username = ''; DROP TABLE users; --'

如果应用执行了这条查询,它会删除 users 表中的所有数据。

防范措施

为了防止 SQL 注入攻击,你应该:

使用参数化查询

参数化查询可以确保用户输入被当作数据而不是 SQL 代码来处理。在 Node.js 中,可以使用像 mysql 包中的 query 方法的参数化版本,或者使用 ORM(对象关系映射)库如 Sequelize 或者 TypeORM

在上述的 Node.js 例子中,你可以使用 mysql 包来执行参数化查询。以下是一个使用 mysql 包和参数化查询来预防 SQL 注入的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...

app.get('/users', (req, res) => {
const username = req.query.username; // 从查询字符串获取用户名

// 构建 SQL 语句
const query = `SELECT * FROM users WHERE username = '?'`;

connection.query(query, [username], (error, results, fields) => {
if (error) throw error;
res.send(results);
});
});

...

在这个例子中,? 是一个参数占位符,代表我们要查询的 username。我们将 username 作为数组 [username] 的元素传递给 query 方法的第二个参数。这样,MySQL 驱动程序就会知道 ? 应该被 username 的值替换。

由于参数化查询确保了用户输入被当作数据处理,而不是作为 SQL 代码的一部分,因此即使 username 包含恶意的 SQL 代码,它也不会被执行。这是预防 SQL 注入攻击的一种有效方式。

使用预编译语句

预编译语句是一种将 SQL 语句和参数分开处理的方式,数据库管理系统会对 SQL 语句进行解析、优化和编译,然后存储起来。每次执行时,只需传递参数,避免了 SQL 注入的风险。

在上述的 Node.js 例子中,你可以使用 mysql 包来执行预编译语句。以下是一个使用 mysql 包和预编译语句来预防 SQL 注入的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...

app.get('/users', (req, res) => {
const username = req.query.username; // 从查询字符串获取用户名

// 准备预编译语句
const prepareQuery = `PREPARE stmt FROM "SELECT * FROM users WHERE username = '?'"`;
connection.query(prepareQuery, (error, results, fields) => {
if (error) throw error;

// 设置参数并执行预编译语句
const executeQuery = 'EXECUTE stmt USING ?';
connection.query(executeQuery, [username], (error, results, fields) => {
if (error) throw error;
console.log(results);

// 释放预编译语句
connection.query('DEALLOCATE PREPARE stmt', (error) => {
if (error) throw error;
connection.end();
});
});
});
});

...

在这个预编译语句的例子中,我们首先准备一个预编译语句,其中包含一个占位符 ?。然后,我们使用 PREPARE 语句将其发送到数据库进行编译。之后,我们使用 EXECUTE 语句并传递参数 [username] 来执行预编译的语句。最后,我们使用 DEALLOCATE PREPARE 语句释放预编译语句的资源。

使用预编译语句的好处是,即使参数中包含恶意的 SQL 代码,它也不会被数据库执行。这是因为预编译语句将参数和 SQL 语句分开处理,数据库只会执行预编译的 SQL 语句,并将参数作为数据来处理。

转义用户输入

转义用户输入是另一种防止 SQL 注入攻击的方法。转义用户输入意味着将用户提供的值中的特殊字符(如单引号、双引号等)替换为它们的转义版本,这样在 SQL 语句中它们就会被当作普通文本处理,而不是 SQL 代码的一部分。

虽然参数化查询是首选的方法,因为它提供了更高级别的安全性,但在某些情况下,你可能需要手动转义用户输入。在 Node.js 中,你可以使用 MySQL 驱动程序的 escape 方法来转义用户输入。

以下是一个使用 Node.jsmysql 驱动程序手动转义用户输入的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

app.get('/users', (req, res) => {
// 从查询字符串获取用户名
const username = req.query.username;

// 使用 escape 方法转义用户输入
const safeUsername = mysql.escape(username);

// 构建 SQL 语句
const query = `SELECT * FROM users WHERE username = '${safeUsername}'`;

connection.query(query, (error, results, fields) => {
if (error) throw error;
res.send(results);
});
});

...

在这个例子中,我们使用了 mysql.escape 方法来转义 username 中的特殊字符。这样,即使 username 包含特殊字符,它也不会被误解为 SQL 语句的一部分。

然而,尽管手动转义用户输入可以提供一定程度的保护,但它仍然不是最安全的做法。原因之一是它可能会导致程序逻辑变得复杂且容易出错,尤其是在构建复杂的 SQL 语句时。因此,推荐使用参数化查询或预编译语句,因为它们提供了更强的安全性保证,并且更容易正确使用。

最小权限原则

确保数据库账户只有执行必要操作的最小权限。例如,如果应用程序只需要从数据库中读取数据,那么就不应该赋予它写入权限。

MySQL 中设置只读权限,你需要登录到MySQL数据库,然后为用户创建一个新的角色并授予只读权限。你可以使用GRANT语句来实现这一点。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 登录到MySQL
mysql -u root -p

-- 创建新角色
CREATE ROLE 'readonlyuser'@'localhost';

-- 授予只读权限
GRANT SELECT ON your_database.* TO 'readonlyuser'@'localhost';

-- 刷新权限
FLUSH PRIVILEGES;

-- 退出MySQL
EXIT;

在上面的例子中,your_database 是你想要让用户具有只读权限的数据库名。'readonlyuser'@'localhost' 表示用户名为 readonlyuser ,并且该用户只能从本地主机连接到数据库。如果你希望用户可以从任何主机连接,你可以将 localhost 替换为 '%'

错误处理

不要将详细的数据库错误信息直接返回给用户。这可以防止攻击者利用这些信息进行更复杂的攻击。

安全审计和测试

使用工具如 SQLMapOWASP Zap 等进行安全审计和测试,确保应用程序不会受到 SQL 注入攻击。

总结

总之, SQL 注入是一种常见的网络安全攻击方式,对数据库的安全构成严重威胁。为了防范 SQL 注入攻击,我们需要加强用户输入验证、使用参数化查询、存储过程等安全措施,并定期更新和修补数据库管理系统和应用程序。只有这样,我们才能确保数据库的安全,保护用户的数据隐私和业务安全。

Web安全之使用 X-Content-Type-Options 首部字段

Web安全之使用 X-Content-Type-Options 首部字段

简介

X-Content-Type-Options 是一个 HTTP 响应头,用于防止浏览器对响应内容的 MIME 类型进行嗅探或猜测。当设置为 nosniff 时,它告诉浏览器应该严格按照响应头中 Content-Type 字段所指定的类型来处理资源,而不应该根据文件扩展名、文件内容或其他因素来尝试重新确定资源的类型。

作用

使用 X-Content-Type-Options 可以提高网站的安全性,因为它可以防止某些类型的攻击,例如 MIME 类型混淆攻击。这种攻击通常涉及将恶意内容伪装成合法的资源类型(如 JavaScript 文件),然后利用浏览器对 MIME 类型的错误处理来执行恶意代码,能够有效的预防 XSS 攻击。

案例

要在你的 web 服务器上设置 X-Content-Type-Options 响应头,你需要根据你的服务器软件(如 Apache、Nginx、IIS 等)进行配置。下面是一些常见的服务器配置示例:

Apache

在 Apache 中,你可以使用 mod_headers 模块来设置响应头。在你的网站配置文件中添加以下行:

1
Header set X-Content-Type-Options "nosniff"

Nginx

在 Nginx 中,你可以在 http、server 或 location 块中添加 add_header 指令来设置响应头:

1
add_header X-Content-Type-Options "nosniff";

IIS

在 IIS 中,你可以使用 web.config 文件来设置响应头。在 <system.webServer> 部分添加以下配置:

1
2
3
4
5
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>

Node.js (使用 Express)

如果你使用 Node.jsExpress 框架,你可以在应用程序中添加中间件来设置响应头:

1
2
3
4
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});

请确保你的服务器配置正确,并且 X-Content-Type-Options 头部已正确设置。这样,浏览器在接收响应时就会遵守该头部的指示,从而提高应用程序的安全性。

Web安全之 CSRF 攻击

Web安全之 CSRF 攻击

简介

CSRF是跨站请求伪造(Cross-Site Request Forgery)的缩写。跨站请求伪造是一种网络攻击方式,攻击者利用已登录的用户在浏览器中存储的合法会话信息,伪造用户发出的请求,以达到欺骗服务器的目的。这种攻击方式并不涉及窃取用户的会话信息,而是利用会话信息。攻击者通常会在用户毫不知情的情况下,在用户的浏览器上执行恶意操作,如更改密码、发布信息、购买商品等。

攻击原理

CSRF的攻击原理基于Web的隐式身份验证机制。在用户访问Web应用时,浏览器会自动携带用户的会话凭证(如Cookie),以便服务器能够识别用户的身份。攻击者通过构造一个恶意网站或链接,诱使用户访问该网站或点击链接,从而触发一个针对目标网站的伪造请求。由于浏览器会自动携带用户的会话凭证,目标网站无法区分正常请求和伪造请求,因此会执行伪造请求中的操作。

攻击目标

CSRF的攻击目标主要是需要用户身份认证的操作,如修改用户信息、更改密码、发表评论、进行转账等。攻击者希望利用用户的身份执行这些操作,以达到窃取数据、篡改内容、转移资金等目的。

案例

  1. 受害者登录 a.com,并保留了登录凭证(Cookie)
  2. 攻击者引诱受害者访问了b.com
  3. b.com 向 a.com 发送了一个请求:a.com/act=xx浏览器会默认携带a.com的Cookie
  4. a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
  5. a.com以受害者的名义执行了act=xx
  6. 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作

例如: 攻击者构造一个伪造的银行转账请求,诱导受害者点击后,该请求会携带受害者的cookie信息,并向正常站点发起请求,从而完成非法转账。或者攻击者设置了一个自动转发邮件的规则,使得受害者的所有邮件都被自动转发到攻击者的邮箱中。

预防方案

CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。

  1. 同源检测:通过 Header 中的 Origin HeaderReferrer Header 确定,但不同浏览器可能会有不一样的实现,不能完全保证

  2. CSRF Token 校验:将 CSRF Token输出到页面中(通常保存在 Session 中),页面提交的请求携带这个 Token ,服务器验证 Token 是否正确

  3. 双重cookie验证

    • 流程:
      • 步骤1:在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw
      • 步骤2:在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw
      • 步骤3:后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
    • 优点:
      • 无需使用Session,适用面更广,易于实施。
      • Token储存于客户端中,不会给服务器带来压力。
      • 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。
    • 缺点:
      • Cookie中增加了额外的字段。
      • 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
      • 难以做到子域名的隔离。
      • 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。
  4. SameSite Cookie属性CookieSameSite属性是一种安全策略,用于限制第三方网站获取Cookie的能力,从而增强网站的安全性。SameSite属性可以设置三个值,分别是:

    • Strict:完全禁止第三方获取Cookie,只有在当前网页的URL与请求目标一致时,才会带上Cookie。这种设置可能会对用户体验造成一定的影响,例如,在跳转到其他网站时,可能会因为未携带Cookie而导致登录状态丢失。
    • Lax:比Strict宽松一些,允许在跨站使用GET请求时携带Cookie。例如,在通过链接进行页面跳转时,会带上Cookie,但在通过表单提交数据时,不会带上Cookie
    • None:允许在任何情况下都携带Cookie。但是,为了安全起见,当设置为None时,必须同时设置Secure属性,即Cookie只能通过HTTPS协议发送,否则设置将无效。
  5. 验证码:在关键操作或敏感操作前,要求用户输入验证码。这样即使攻击者能够构造伪造请求,也无法自动执行操作,因为需要用户手动输入验证码。

总结

CSRF是一种严重的网络威胁,对于需要用户身份认证的操作尤其危险。为了保障用户数据的安全性和隐私保护,开发人员在设计和开发网络应用时,应该充分考虑CSRF攻击的防御措施,并采取多种手段提高系统的安全性。同时,用户也应该保持警惕,避免点击不明来源的链接或访问可疑网站,以防止CSRF攻击的发生。

Web安全之 XSS 攻击

Web安全之 XSS 攻击

简介

XSS,全称跨站脚本攻击(Cross-Site Scripting),是一种网络安全漏洞攻击,指攻击者在网页中嵌入恶意脚本,当其他用户浏览该网页时,恶意脚本就会在其浏览器上执行,从而达到攻击者窃取用户信息、破坏数据、篡改网页内容、在用户浏览器上执行非法任务等目的。

分类

XSS攻击分为三种类型:

  • 反射型XSS(Reflected XSS):攻击者将恶意脚本嵌入到URL地址中,当其他用户访问这个URL时,恶意脚本就会在其浏览器中执行。这种攻击方式需要用户主动点击含有恶意脚本的链接才会触发。

  • 存储型XSS(Stored XSS):攻击者将恶意脚本存储到被攻击的网站数据库中,当其他用户访问网站时,恶意脚本会从数据库中取出并在用户浏览器中执行。这种攻击方式不需要用户主动点击链接,只要用户浏览被攻击的网站就可能被攻击。

  • DOM型XSS(DOM-based XSS):攻击者通过修改页面的DOM结构,注入恶意脚本,当其他用户浏览该页面时,恶意脚本会在用户浏览器中执行。这种攻击方式也不需要用户主动点击链接,只需要用户浏览被修改的页面就可能被攻击。

案例

反射型XSS

Node.js中,一个反射型XSS攻击的案例可能涉及一个web应用,该应用没有正确地处理或转义用户输入的数据,并将其直接插入到HTML响应中。攻击者可以构造一个包含恶意脚本的URL,当其他用户访问这个URL时,恶意脚本会在用户的浏览器中执行。

以下是一个简单的Node.js反射型XSS攻击的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const app = express();
app.use(express.static('public'));

app.get('/profile', (req, res) => {
// 假设用户可以通过URL参数传递他们的名字
const username = req.query.username;

// 没有对用户输入进行任何处理或转义
const html = `<html>
<body>
<h1>Welcome, ${username}!</h1>
</body>
</html>`;

res.send(html);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

在这个例子中,我们创建了一个简单的Express应用,其中有一个/profile路由,该路由从URL的查询参数中获取用户名(username)。然后,我们将这个用户名嵌入到一个HTML字符串中,并将其作为响应发送回客户端。

攻击者可以构造一个包含恶意脚本的URL,如:

1
http://example.com/profile?username=<script>alert('XSS');</script>

当用户访问这个URL时,浏览器会接收到包含恶意脚本的HTML响应,并执行该脚本,从而触发XSS攻击。

为了防止反射型XSS攻击,开发者应该对用户输入进行适当的过滤和转义。在这个案例中,可以使用如escape-html这样的库来转义HTML特殊字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const escapeHtml = require('escape-html');

app.get('/profile', (req, res) => {
const username = req.query.username;

// 转义用户输入中的HTML特殊字符
const escapedUsername = escapeHtml(username);

const html = `<html>
<body>
<h1>Welcome, ${escapedUsername}!</h1>
</body>
</html>`;

res.send(html);
});

通过转义用户输入,恶意脚本将不会被浏览器执行,从而防止了XSS攻击。此外,开发者还应该使用内容安全策略(CSP)等额外安全措施来增强应用的安全性。

Read More

常用的CSS技巧

  1. 网站平滑滚动

<html>元素中添加scroll-behavior: smooth,以实现整个页面的平滑滚动。

1
2
3
html {
scroll-behavior: smooth;
}
  1. 链接的属性选择器

此选择器的目标是具有以“https”开头的 href 属性的链接。

1
2
3
a[href^="https"] {
color: blue;
}
  1. 〜合并兄弟姐妹

选择 <h2> 后面的所有兄弟元素 <p> 元素。

1
2
3
h2 ~ p {
color: blue;
}
  1. :not() 伪类

该选择器将样式应用于不具有“特殊”类的列表项。

1
2
3
li:not(.special) {
font-stlye: italic;
}
  1. 用于响应式排版的视口单位

使用视口单位(vw、vh、vmin、vmax)可以使字体大小响应视口大小。

1
2
3
h1 {
font-size: 5vw;
}
  1. :empty 表示空元素

此选择器定位空的 <p> 元素并隐藏它们。

1
2
3
p:empty {
display: none;
}
  1. 自定义属性(变量)

可以定义和使用自定义属性,以更轻松地设置主题和维护。

1
2
3
4
5
6
7
:root {
--main-color: #3498db;
}

h1 {
color: var(--main-color);
}
  1. 图像控制的Object-fit属性

object-fit 控制如何调整替换元素(如 <img>)的内容大小。

1
2
3
4
5
img {
width: 100px;
height: 100px;
object-fit: cover;
}

Read More

Service Worker:提升网页性能与用户体验的关键技术

Service Worker:提升网页性能与用户体验的关键技术

随着互联网的快速发展,网页应用的复杂性不断提高,对性能和用户体验的要求也日益增强。为了应对这些挑战,浏览器技术也在不断进步,其中 Service Worker 作为一种在浏览器后台独立运行的脚本,成为了优化网页性能、提升用户体验的关键技术。

基本概念

Service Worker 是一种运行在浏览器后台的脚本,它独立于网页内容,不会阻塞页面的渲染。它允许开发人员在浏览器和网络之间设置一个代理服务器,拦截和处理网络请求,实现缓存、推送通知、消息传递等功能。Service Worker 使得网页能够在离线状态下运行,提高页面加载速度,优化用户体验。

caniuse_service_worker

生命周期

Service Worker 的生命周期完全独立于网页。

Service Worker 被注册成功后,它将开始它的生命周期,我们对 Service Worker 的操作一般都是在其生命周期里面进行的。Service Worker 的生命周期分为下面几个状态:installing -> installed -> activating -> activated -> redundant。

f54bdb55c775be0498c6efcd55a73020

  1. installing(安装):这个状态发生在 Service Worker 注册之后,表示开始安装,这个状态会触发 install 事件,一般会在 install 事件的回调里面进行静态资源的离线缓存, 如果这些静态资源缓存失败了,那 Service Worker 安装就会失败,生命周期终止。

  2. installed(安装后):当成功捕获缓存到的资源时,Service Worker 会变为这个状态,当此时没有其他的 Service Worker 线程在工作时,会立即进入激活状态,如果此时有正在工作的 Service Worker 工作线程,则会等待其他的 Service Worker 线程被关闭后才会被激活。可以使用 self.skipWaiting() 方法强制正在等待的 Service Worker 工作线程进入激活状态。

  3. activating(激活):在这个状态下会触发 activate 事件,在 activate 事件的回调中去清理旧版缓存。

  4. activated(激活后):在这个状态下,Service Worker 会取得对整个页面的控制

  5. redundant(废弃状态):这个状态表示一个 Service Worker 的生命周期结束。新版本的 Service Worker 替换了旧版本的 Service Worker 会出现这个状态

常用接口

  • Cache
    表示用于 Request / Response 对象对的存储,作为 Service Worker 生命周期的一部分被缓存。

  • CacheStorage
    表示 Cache 对象的存储。提供一个所有命名缓存的主目录,Service Worker 可以访问并维护名字字符串到 Cache 对象的映射。

  • Client
    表示 service worker client 的作用域。一个 service worker client 可以是浏览器上下文的一个文档,也可以是一个由 active worker 控制的 Shared Worker

  • Clients
    表示一个 Client 对象容器;是访问当前源的活动的 service worker client 的主要途径。

  • ExtendableEvent
    扩展被分发到 ServiceWorkerGlobalScopeinstallactivate 事件时序,作为 service worker 生命周期的一部分。这会确保任何功能型事件(如 FetchEvent)不被分发到 Service Worker,直到它更新了数据库架构、删除过期缓存项等等以后。

  • ExtendableMessageEvent
    Service Worker 触发的 message 事件的时间对象(当 ServiceWorkerGlobalScope 从另一个上下文收到通道消息),延长了此类事件的生命周期。

  • FetchEvent
    传递给 ServiceWorkerGlobalScope.onfetch 处理函数的参数,FetchEvent 代表一个在 Service WorkerServiceWorkerGlobalScope 中分发的请求动作。它包含关于请求和响应的结果信息,并且提供 FetchEvent.respondWith() 方法,这个方法允许我们提供任意的响应返回到控制页面。

  • NavigationPreloadManager
    提供与 Service Worker 一起管理资源预加载的方法。

  • Navigator.serviceWorker
    返回一个 ServiceWorkerContainer 对象,该对象提供对相关 document 的注册、删除、更新以及与 Service Worker 对象通信的访问。

  • NotificationEvent
    传递给 onnotificationclick 处理函数的参数,NotificationEvent 接口代表在 Service WorkerServiceWorkerGlobalScope 中分发的单击事件通知。

  • ServiceWorker
    表示一个 Service Worker。多个浏览的上下文 (例如 pageworker 等等) 都能通过相同的 Service Worker 对象相关联。

  • ServiceWorkerContainer
    提供一个在网络生态中把 service worker 作为一个整体的对象,包括辅助注册,反注册以及更新 service worker,并且访问 service worker 的状态以及他们的注册信息。

  • ServiceWorkerGlobalScope
    表示 service worker 的全局执行上下文。

  • MessageEvent
    表示发送到 ServiceWorkerGlobalScope 的信息。

  • ServiceWorkerRegistration
    表示 service worker 的注册。

  • WindowClient
    表示在浏览器上下文中记录的 service worker 客户端的作用域,被活动的工作者控制。是 Client 对象的特殊类型,包含一些附加的方法和可用的属性。

使用场景

缓存管理

Service Worker 可以拦截和处理网络请求,将资源缓存在本地,实现离线访问和快速加载。开发人员可以根据需求自定义缓存策略,提高网页的加载速度和性能。

下面是一个简单的 Service Worker 缓存管理的例子,用于缓存和提供静态资源。

首先,你需要注册一个 Service Worker:

1
2
3
4
5
6
// 在你的主 JavaScript 文件中,例如 main.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(() => console.log('Service Worker 已注册'))
.catch(err => console.error('Service Worker 注册失败:', err));
}

然后,创建一个 service-worker.js 文件,用于定义缓存策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// service-worker.js

self.addEventListener('install', (event) => {
// Perform install steps
event.waitUntil(
caches.open('my-cache').then((cache) => {
// 打开一个名为 'my-cache' 的缓存
return cache.addAll([
'/',
'/styles/main.css',
'/script/main.js'
// 你可以添加更多需要缓存的资源
]);
})
);
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(
(res) => {
// Check if we received a valid response
if (!res || res.status !== 200 || res.type !== 'basic') {
return response;
}
// Important: Clone the response. A response is a stream and
// can only be consumed once.
const responseToCache = res.clone();
caches.open('my-cache').then((cache) => {
// Take the response from the network,
// and put a copy in the cache.
cache.put(event.request, responseToCache);
});
return res;
}
);
})
);
});

这个例子中,当 Service Worker 安装时,它会打开一个名为 'my-cache' 的缓存,并添加一些资源到缓存中。当浏览器请求这些资源时,Service Worker 会首先检查缓存中是否有这些资源。如果有,它会直接从缓存中提供这些资源,而不是从网络上获取。如果缓存中没有这些资源,Service Worker 会从网络上获取它们,然后将它们添加到缓存中,以便下次可以直接从缓存中提供。

注意,为了简化示例,这里并没有处理所有可能的错误情况,也没有考虑缓存的更新和失效策略。在实际应用中,你可能需要根据你的具体需求来定制你的缓存策略。

推送通知

Service Worker 可以接收来自服务器的推送消息,即使网页未打开或处于休眠状态,也能向用户发送通知。这一功能使得开发者能够及时向用户传达重要信息,提高应用的活跃度和用户粘性。

下面是一个使用 Service WorkerPush API 发送推送通知的基本例子。

首先,你需要注册 Service Worker 并订阅推送消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// main.js

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('Service Worker 注册成功:', registration.scope);

// 检查浏览器是否支持显示通知
if (!('Notification' in window)) {
alert('此浏览器不支持桌面通知');
}

// 检查是否已授予权限
else if (Notification.permission === 'granted') {
// 如果用户已授予权限,我们可以立即注册推送
subscribeUser();
}

// 否则,我们需要询问用户
else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(function (permission) {
if (permission === 'granted') {
subscribeUser();
}
});
}

// 订阅推送通知
function subscribeUser() {
return registration.pushManager.subscribe({
userVisibleOnly: true
})
.then(function(subscription) {
console.log('用户已订阅推送通知:', subscription);

// 将订阅信息发送到服务器
sendSubscriptionToServer(subscription);
})
.catch(function(err) {
console.error('无法订阅推送通知:', err);
});
}

// 将订阅信息发送到服务器
function sendSubscriptionToServer(subscription) {
// TODO: 将 subscription 发送到你的服务器
console.log('将订阅信息发送到服务器:', subscription);
}
})
.catch(function(err) {
console.error('Service Worker 注册失败:', err);
});
}

然后,你需要一个 Service Worker 脚本 (service-worker.js) 来接收和显示推送通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// service-worker.js

self.addEventListener('push', function(event) {
console.log('收到推送消息:', event);

// 你可能需要从服务器获取一些数据来显示通知
// 这里只是一个简单的示例
var title = '推送通知';
var body = '你收到了一条新消息!';
var icon = '/images/icon.png';

event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon
})
);
});

self.addEventListener('notificationclick', function(event) {
console.log('用户点击了通知:', event);

// 用户点击通知后,你可以执行一些操作,例如打开一个新的页面
event.notification.close();

// 例如,打开一个特定的 URL
event.waitUntil(
clients.openWindow('https://example.com/notification-clicked')
);
});

请注意,为了实际使用推送通知,你还需要一个服务器端的组件来发送推送消息。这通常涉及到使用 Web Push Protocol (Web Push) 与用户的浏览器进行通信。此外,你可能需要配置你的服务器以接收订阅信息,并在需要时发送推送消息。

此外,由于推送通知和 Service Worker 的复杂性,这里提供的代码只是一个起点。在生产环境中,你可能需要处理更多细节,如错误处理、用户权限管理、通知内容的动态生成等。

消息传递

Service Worker 可以与其他浏览器环境(如页面、扩展程序等)进行双向通信,实现消息传递和共享数据。这使得开发者能够在不同环境之间传递信息,实现更复杂的功能和更好的用户体验。

下面是一个简单的 Service Worker 消息传递的例子。

首先,注册并启动 Service Worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// main.js

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration.scope);

// 向 Service Worker 发送消息
registration.active.postMessage({ action: 'hello' });

// 监听来自 Service Worker 的消息
navigator.serviceWorker.addEventListener('message', event => {
console.log('收到来自 Service Worker 的消息:', event.data);
if (event.data.action === 'response') {
alert('Service Worker 回应了: ' + event.data.message);
}
});
})
.catch(err => {
console.error('Service Worker 注册失败:', err);
});
}

然后,在 service-worker.js 中处理接收到的消息,并发送响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// service-worker.js

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('my-cache').then((cache) => {
// 缓存逻辑
})
);
});

self.addEventListener('activate', (event) => {
// 激活逻辑
});

self.addEventListener('fetch', (event) => {
// 拦截请求和响应的逻辑
});

self.addEventListener('message', (event) => {
console.log('收到来自页面的消息:', event.data);
if (event.data.action === 'hello') {
// 向页面发送回应
event.ports[0].postMessage({ action: 'response', message: '你好,页面!' });
}
});

在这个例子中,当页面加载并成功注册 Service Worker 后,它会向 Service Worker 发送一个包含 action: 'hello' 的消息。Service Worker 监听到这个消息后,会打印出收到的消息内容,并通过 event.ports[0].postMessage 向页面发送一个回应消息,其中包含了 action: 'response' 和一条消息字符串。

页面通过监听 message 事件来接收来自 Service Worker 的回应,并在控制台中打印出消息内容。如果回应消息中的 action'response',页面会弹出一个警告框显示收到的消息。

请注意,postMessage API 允许在 Service Worker 和页面之间传递结构化数据,如对象、数组、字符串等。但是,出于安全考虑,不能直接传递函数、DOM 对象或其他某些类型的数据。此外,event.ports 提供了一个双向通信的通道,允许 Service Worker 和页面之间进行更复杂的交互。

优缺点

优点:

  1. 提高网页性能:通过缓存管理,Service Worker 可以减少网络请求,降低带宽消耗,提高网页加载速度。
  2. 提升用户体验:Service Worker 可以实现离线访问、推送通知等功能,提高用户的满意度和粘性。
  3. 拓展性强:Service Worker 可以与其他浏览器环境进行通信,为开发者提供更多可能性。

缺点:

  1. 兼容性问题:虽然主流浏览器都支持 Service Worker ,但在一些旧版本或非主流浏览器中可能无法使用。
  2. 学习成本高:使用 Service Worker 需要一定的技术储备和学习成本,对于初学者来说可能有一定的难度。

总结

Service Worker 作为一种在浏览器后台运行的脚本技术,为优化网页性能和提升用户体验提供了有力支持。通过缓存管理、推送通知和消息传递等功能,Service Worker 使得网页能够在离线状态下运行,提高加载速度,及时向用户传达重要信息,实现更复杂的功能和更好的用户体验。虽然存在一些兼容性和学习成本的问题,但随着技术的不断发展和普及,相信 Service Worker 将在未来的网页开发中发挥越来越重要的作用。

H5 移动端调试工具汇总

H5 移动端调试工具汇总

一、概要

因为移动端操作系统分为 iOSAndroid 两派,所以本文的调试技巧也会按照不同的系统来区分。寻找最合适高效的方式,才能让你事半功倍。

文章会列举目前适合移动端调试的多种方案,快来选择你的最佳实践吧!

二、iOS 设备

Safari:iphone 调试利器,查错改样式首选,需要我们做如下设置:

  • 浏览器设置:Safari - 偏好设置 - 高级 - 勾选「在菜单栏中显示开发」菜单
  • iphone 设置:设置 - Safari - 高级 - 打开 Web 检查器

大功告成,这时候通过手机的 Safari 来打开 H5 页面,我们通过浏览器开发选项可以看到:

image

iOS 模拟器:不需要真机,适合调试 Webview 和 H5 有频繁交互的功能页面。

首先下载 Xcode ,运行项目,选择模拟器 iphonex,编译后就会打开模拟器,如下:

image

可以看到 H5 已经在「壳子」中运行起来了,下来就可以尝试调用 Webview 的方法,和「壳子」交互了。

具体的调试功能还是依赖浏览器的开发选项,与上无异,就不赘述了。

Read More

使用 git 克隆 github 上的项目失败

现象

今天在使用 git clone nextjs demo project 源代码的时, git clone https://github.com/Weibozzz/next-blog.git 下载速度很慢,然后下载一段时间后,总是提示下面的错误信息

1
2
3
4
5
6
7
8
nCloning into 'next-blog'...
remote: Enumerating objects: 111, done.
remote: Counting objects: 100% (111/111), done.
remote: Compressing objects: 100% (83/83), done.
error: RPC failed; curl 18 transfer closed with outstanding read data remaining
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed

原因

由于Http协议错误,当 pull 或者 clone 的时候,或者是 github 某个CDN被伟大的墙屏蔽所致。

解决办法

协议错误

  1. 先执行下列命令

    1
    git config --global http.postBuffer 524288000
  2. 再执行git pull 或者 git clone命令

墙屏蔽

  1. 访问 http://github.global.ssl.fastly.net.ipaddress.com/#ipinfo
    获取cdn域名以及IP地址

  2. 访问 http://github.com.ipaddress.com/#ipinfo 获取cdn域名以及IP地址 cdn域名以及IP地址

  3. 将上述获取的IP地址添加到/etc/hosts

    1
    sudo vim /etc/hosts

    添加IP地址到hosts

  4. 刷新dns缓存

    1
    2
    sudo killall -HUP mDNSResponder
    sudo dscacheutil -flushcache

结果

再执行 git clone 操作的时候,速度飕飕飕的上去了,一下子达到几百Kb啦~

使用Weinre调试移动端Web开发

一、使用Npm全局安装weinre

1
npm -g install weinre

二、引入js文件

1
<script src="http://192.168.225.198:8081/target/target-script-min.js#anonymous"></script>

三、启动 Weinre Debug 服务端

1
weinre --httpPort 8081 --boundHost -all-

常用的shell命令

一、SSH到服务器上再执行shell命令

1
2
3
4
5
ssh -t root@192.168.111.111 \
"sudo cp -rf ${REMOTE_DESTS[$key]#*:}/${NGINX_CONF_FILENAME} /etc/nginx/sites-available/ &&
sudo rm -rf ${REMOTE_DESTS[$key]#*:}/${NGINX_CONF_FILENAME} &&
sudo ln -sf /etc/nginx/sites-available/${NGINX_CONF_FILENAME} /etc/nginx/sites-enabled/${NGINX_CONF_FILENAME} &&
sudo service nginx reload"

二、查看或修改监控文件系统(Inotify)的watch数目

1
2
3
4
5
# 设置
sudo sysctl fs.inotify.max_user_watches=524288

# 查看
sysctl -a | grep inotify

三、查看Ubuntu操作系统位数及版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看Ubuntu操作系统位数
# 方法一: getconf
getconf LONG_BIT
# 64 or 32

# 方法二: uname -a
uname -a
# Linux user-3020 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
# i686表示32位, x86_64表示64位

# 查看Ubuntu操作系统版本
lsb_release -a
# Distributor ID: Ubuntu
# Description: Ubuntu 14.04.2 LTS
# Release: 14.04
# Codename: trusty

四、动态查看一个文件

1
tail -f filename

五、sudo操作手动输入密码

1
echo "abc123_" | sudo -S sh -c "cp /home/user/hosts /etc/hosts"

六、替换变量中字符

第一种方式

1
COMMIT_MSG=$(COMMIT_MSG//feat/build) # 将COMMIT_MSG中所有的feat替换成build

第二种方式: sed

1
COMMIT_MSG=$(echo $COMMIT_MSG | sed 's/^ //g') #去除COMMIT_MSG变量中所有左边的空格

第三种方式: tr命令

tr命令可以对来自标准输入的字符进行替换、压缩和删除。它可以将一组字符变成另一组字符,经常用来编写优美的单行命令,作用很强大。

语法

1
tr(选项)(参数)

选项

  • -c或——complerment:取代所有不属于第一字符集的字符;
  • -d或——delete:删除所有属于第一字符集的字符;
  • -s或–squeeze-repeats:把连续重复的字符以单独一个字符表示;
  • -t或–truncate-set1:先删除第一字符集较第二字符集多出的字符。

参数

  • 字符集1:指定要转换或删除的原字符集。当执行转换操作时,必须使用参数“字符集2”指定转换的目标字符集。但执行删除操作时,不需要参数“字符集2”;

*字符集2:指定要转换成的目标字符集。

实例

将输入字符由大写转换为小写:

1
2
echo "HELLO WORLD" | tr 'A-Z' 'a-z'
hello world

‘A-Z’ 和 ‘a-z’都是集合,集合是可以自己制定的,例如:’ABD-}’、’bB.,’、’a-de-h’、’a-c0-9’都属于集合,集合里可以使用’\n’、’\t’,可以可以使用其他ASCII字符。

更详细的tr实例

七、if指令详解(TODO)

判断文件夹是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 如果文件夹不存在,创建文件夹
if [ ! -d "/myfolder" ]; then
mkdir /myfolder
fi

# shell判断文件,目录是否存在或者具有权限

folder="/var/www/"
file="/var/www/log"

# -x 参数判断 $folder 是否存在并且是否具有可执行权限
if [ ! -x "$folder"]; then
mkdir "$folder"
fi

# -d 参数判断 $folder 是否存在
if [ ! -d "$folder"]; then
mkdir "$folder"
fi

# -f 参数判断 $file 是否存在
if [ ! -f "$file" ]; then
touch "$file"
fi

# -n 判断一个变量是否有值
if [ ! -n "$var" ]; then
echo "$var is empty"
exit 0
fi

# 判断两个变量是否相等
if [ "$var1" = "$var2" ]; then
echo '$var1 eq $var2'
else
echo '$var1 not eq $var2'
fi

八、开始行和结束行的内容

1
2
3
4
5
6
7
8
9
10
11
# 文件最后100行
tail -n 100 file

# 文件开头100行
head -n 100 file

# 文件指定开始行和结束行的内容(包头不包尾)
sed '1,100p' file

# 文件有多少行
wc -l file

九、用来从文件或者变量中提取字段(awk)

awk 用来从文本文件中提取字段。缺省地,字段分割符是空格,可以使用-F指定其他分割符。

1
2
echo "Adam Bor, 34, IndiaKerry Miller, 22, USA" | awk -F, '{print $1 "," $3 }'
# Adam Bor, IndiaKerry Miller, USA

这里我们使用,作为字段分割符,同时打印第一个和第三个字段

十、输出不换行(echo)

echo的参数中, -e表示开启转义, /c表示不换行,脚本如下:

1
2
3
4
5
#!/bin/sh
#filename: 1
echo -e "please input a value:\c"
read value
# echo "what you input is:" $valuel

脚本2:

1
2
3
4
5
#!/bin/sh
#filename: 1
echo -n "please input a value:"
read value
echo "what you input is:" $value

十一、字符串大小写不敏感的比较

通用的方法是将字符串先转换成小写后再比较

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

xxx="Temp"
yyy="temp"

x_tmp=$(echo $xxx | tr [A-Z] [a-z])
y_tmp=$(echo $yyy | tr [A-Z] [a-z])

if [ "$x_tmp " = "$y_tmp " ]; then
echo "PASS"
else
echo "FAIL"
fi

十二、**&&** 运算符

语法格式:

1
command1 && command2 [&& command3 ...]

&&左边的命令(命令1)返回真(即返回0,成功被执行)后,&&右边的命令(命令2)才能够被执行;换句话说,“如果这个命令执行成功&&那么执行这个命令”。

1 命令之间使用 && 连接,实现逻辑与的功能。
2 只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。
3 只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行。

十三、**||** 运算符

语法格式:

1
command1 || command2 [|| command3 ...]

||则与&&相反。如果||左边的命令(命令1)未执行成功,那么就执行||右边的命令(命令2);或者换句话说,“如果这个命令执行失败了||那么就执行这个命令。

1 命令之间使用 || 连接,实现逻辑或的功能。
2 只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才会被执行。这和 c 语言中的逻辑或语法功能相同,即实现短路逻辑或操作。
3 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。

十四、判断变量中是否包含某个字符串

3 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。

1
2
3
str="this is a string"
[[ $str =~ "this" ]] && echo "$str contains this"
[[ $str =~ "that" ]] || echo "$str does NOT contain that"

结果为:
this is a string contains this
this is a string does NOT contains that
“[[“ 判断命令和 “=~”正则式匹配符号

十五、shell中 [ ][[ ]] 的区别

http://blog.csdn.net/ysdaniel/article/details/7905818

十六、shell中 ‘’ , “”`` 的区别

http://www.cnblogs.com/Skyar/p/5914942.html

参考地址:

基础语法
echo指令