明天是星期天

你不会找到路,除非你敢于迷路

使用 css

1
2
3
4
5
6
7
8
9
10
* {
margin: 0;
padding: 0;
}
:root {
font-size: calc(100vw / 750);
}
body {
font-size: 16px;
}

以设计图750为例,1px = 1rem

研究 HTTPS 的双向认证实现与原理,踩了不少坑,终于整个流程都跑通了,把一些心得,特别是容易踩坑的地方记录下来

原理

双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,在建立 Https 连接的过程中,握手的流程比单向认证多了几步。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输

单向认证流程

  • 客户端发起建立 HTTPS 连接请求,将 SSL 协议版本的信息发送给服务器端
  • 服务器端将本机的公钥证书(server.crt)发送给客户端
  • 客户端读取公钥证书(server.crt),取出了服务端公钥
  • 客户端生成一个随机数(密钥 R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端
  • 服务端用自己的私钥(server.key)去解密这个密文,得到了密钥 R
  • 服务端和客户端在后续通讯过程中就使用这个密钥 R 进行通信了

双向认证流程

  • 客户端发起建立 HTTPS 连接请求,将 SSL 协议版本的信息发送给服务端
  • 服务器端将本机的公钥证书(server.crt)发送给客户端
  • 客户端读取公钥证书(server.crt),取出了服务端公钥
  • 客户端将客户端公钥证书(client.crt)发送给服务器端
  • 服务器端使用根证书(root.crt)解密客户端公钥证书,拿到客户端公钥
  • 客户端发送自己支持的加密方案给服务器端
  • 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后,发送给客户端
  • 客户端使用自己的私钥解密加密方案,生成一个随机数 R,使用服务器公钥加密后传给服务器端
  • 服务端用自己的私钥去解密这个密文,得到了密钥 R
  • 服务端和客户端在后续通讯过程中就使用这个密钥 R 进行通信了

生成证书

如果要把整个双向认证的流程跑通,最终需要六个证书

  • 服务器端公钥证书:server.crt
  • 服务器端私钥文件:server.key
  • 根证书:root.crt
  • 客户端公钥证书:client.crt
  • 客户端私钥文件:client.key
  • 客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12

生成这一些列证书之前,我们需要先生成一个 CA 根证书,然后由这个 CA 根证书颁发服务器公钥证书和客户端公钥证书,为了验证根证书颁发与验证客户端证书这个逻辑,我们使用根证书办法两套不同的客户端证书,然后同时用两个客户端证书来发送请求,看服务器端是否都能识别

我们可以全程使用 openssl 来生成一些列的自签名证书,自签名证书没有听过证书机构的认证,很多浏览器会认为不安全,但我们用来实验是足够的。需要在本机安装了 openssl 后才能继续本章的实验

生成自签名根证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(1)创建根证书私钥:
openssl genrsa -out root.key 1024

(2)创建根证书请求文件:
openssl req -new -out root.csr -key root.key
后续参数请自行填写,下面是一个例子:
Country Name (2 letter code) [XX]:cn
State or Province Name (full name) []:bj
Locality Name (eg, city) [Default City]:bj
Organization Name (eg, company) [Default Company Ltd]:alibaba
Organizational Unit Name (eg, section) []:test
Common Name (eg, your name or your servers hostname) []:root
Email Address []:a.alibaba.com
A challenge password []:
An optional company name []:

(3)创建根证书:
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650

在创建证书请求文件的时候需要注意三点,下面生成服务器请求文件和客户端请求文件均要注意这三点:

  1. 根证书的 Common Name 填写 root 就可以,所有客户端和服务器端的证书这个字段需要填写域名,一定要注意的是,根证书的这个字段和客户端证书、服务器端证书不能一样
  2. 其他所有字段的填写,根证书、服务器端证书、客户端证书需保持一致
  3. 最后的密码可以直接回车跳过

经过上面三个命令行,我们最终可以得到一个签名有效期为 10 年的根证书 root.crt,后面我们可以用这个根证书去颁发服务器证书和客户端证书

生成自签名服务器端证书

1
2
3
4
5
6
7
8
(1)生成服务器端证书私钥:
openssl genrsa -out server.key 1024

(2) 生成服务器证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out server.csr -key server.key

(3) 生成服务器端公钥证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650

经过上面的三个命令,我们得到:

  • server.key:服务器端的秘钥文件
  • server.crt:有效期十年的服务器端公钥证书,使用根证书和服务器端私钥文件一起生成

生成自签名客户端证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(1)生成客户端证书秘钥:
openssl genrsa -out client.key 1024
openssl genrsa -out client2.key 1024

(2) 生成客户端证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out client.csr -key client.key
openssl req -new -out client2.csr -key client2.key

(3) 生客户端证书
openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
openssl x509 -req -in client2.csr -out client2.crt -signkey client2.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650


(4) 生客户端p12格式证书,需要输入一个密码,选一个好记的,比如123456
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
openssl pkcs12 -export -clcerts -in client2.crt -inkey client2.key -out client2.p12

重复使用上面的三个命令,我们得到两套客户端证书:

  • client.key/client2.key:客户端的私钥文件
  • client.crt/client2.key:有效期十年的客户端证书,使用根证书和客户端私钥一起生成
  • client.p12/client2.p12:客户端 p12 格式,这个证书文件包含客户端的公钥和私钥,主要用来给浏览器访问使用

服务端配置(app.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
const https = require("https");
const fs = require("fs");

const options = {
key: fs.readFileSync("./cert/server.key"),
cert: fs.readFileSync("./cert/server.crt"),
// (双向认证)添加可信任的CA证书,用于验证客户端证书
ca: fs.readFileSync("./cert/root.crt"),
// passphrase: "146116", 如果证书需要密码则使用密码

// 使用客户端证书验证
requestCert: true,
// 如果没有请求到客户端来自信任CA颁发的证书,拒绝客户端的连接
rejectUnauthorized: true,
};
const port = 443;
https
.createServer(options, (req, res) => {
console.log("server connected", res.connection.authorized ? "authorized" : "unauthorized");
res.writeHead(200);
res.end("hello world!\n");
})
.listen(port, () => {
console.log(`running server https://127.0.0.1`);
});

// 使用客户端连接测试
require("./client");

客户端配置(client.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
const https = require("https");
const fs = require("fs");

var options = {
host: "localhost",
port: 443,
path: "/",
method: "GET",
// (双向认证)请求使用客户端证书,使服务器信任请求
key: fs.readFileSync("./cert/client.key"),
cert: fs.readFileSync("./cert/client.crt"),

// (单向认证)添加可信任根证书,验证服务端证书 验证是否可信任
ca: [fs.readFileSync("./cert/root.crt")],

rejectUnauthorized: true, // 如果验证失败,则断开请求
agent: false, // 仅为此一个请求创建一个新代理
};
var req = https.request(options, function (res) {
console.log("server authorize status: " + res.socket.authorized);
res.on("data", function (d) {
console.log("响应数据: " + d);
});
});
req.end();
req.on("error", function (e) {
console.error(e);
});

要注意的地方

  • 根证书的 Common Name 填写 root 就可以,所有客户端和服务器端的证书这个字段需要填写域名,一定要注意的是,根证书的这个字段和客户端证书、服务器端证书不能一样
  • server 端的 ca 需要配置根证书 root.crt,而不是客户端证书,client 端的 ca 需要配置根证书 root.crt,而不是服务端证书

pnpm

  • 快速 比 npm 和 Yarn 更快
  • 高效 一个版本的软件包只能在磁盘上保存一次
  • 确定性 有一个名为 shrinkwrap.yaml 的锁定文件
  • 严格 一个包只能访问 package.json 中指定的依赖关系
  • 无处不在 适用于 Windows,Linux 和 OS X. 挂钩, node_modules 不再是黑盒子了
  • 别名 安装相同软件包的不同版本或使用不同的名称导入它

安装

1
npm install -g pnpm

node_modules

为什么 pnpm 的 node_modules 会不同寻常? 我们创建两个路径并在其中一个执行 npm add express 然后在另一个中执行 pnpm add express

npm add express 的 node_modules 中得到的顶级目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.bin
accepts
array-flatten
body-parser
bytes
content-disposition
cookie-signature
cookie
debug
depd
destroy
ee-first
encodeurl
escape-html
etag
express

pnpm add express 的 node_modules 中得到的顶级目录

1
2
3
.pnpm
.modules.yaml
express

所以,所有的依赖去哪了呢? node_modules 中只有一个叫 .pnpm 的文件夹以及一个叫做 express 的软链,我们只安装了 express,所以它是唯一一个你的应用必须拥有访问权限的包

我们看看在 express 中都有些什么

1
2
3
4
5
6
7
8
9
10
▾ node_modules
.pnpm
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
.modules.yaml

express 没有 node_modules? express 的所有依赖都去哪里了? 诀窍是 express 只是一个软链。 当 Node.js 解析依赖的时候,它使用这些依赖的真实位置,所以它不保留软链

如何通过点击某个元素,控制其他元素,不用 Javascript,使用纯 CSS 方案,实现类似下图的导航栏切换

:target 伪类选择器

首先,我们要解决的问题是如何接收点击事件,这里第一种方法我们采用 :target 伪类接收

:target 是 CSS3 新增的一个伪类,可用于选取当前活动的目标元素。当然 URL 末尾带有锚名称 #,就可以指向文档内某个具体的元素,这个被链接的元素就是目标元素(target element),它需要一个 id 去匹配文档中的 target

假设我们的 HTML 代码如下

1
2
3
4
5
6
<ul class="nav">
<li>列表1</li>
<li>列表2</li>
</ul>
<div>列表1内容:123456</div>
<div>列表2内容:abcdefgkijkl</div>

由于要使用 :target,需要 HTML 锚点,以及锚点对应的 HTML 片段。所以上面的结构要变成

1
2
3
4
5
6
<ul class="nav">
<li><a href="#content1">列表1</a></li>
<li><a href="#content2">列表2</a></li>
</ul>
<div id="content1">列表1内容:123456</div>
<div id="content2">列表2内容:abcdefgkijkl</div>

这样,上面 中的锚点 #content1 就对应了列表 1

,锚点 2 与之相同对应列表 2,接下来,我们就可以使用 :target 接受到点击事件,并操作对应的 DOM 了

1
2
3
4
5
6
7
8
9
#content1,
#content2 {
display: none;
}

#content1:target,
#content2:target {
display: block;
}

上面的 CSS 代码,一开始页面中的 #content1 与 #content2 都是隐藏的,当点击列表 1 触发 href=”#content1” 时,页面的 URL 会发生变化

  • 由 www.example.com 变成 www.example.com#content1
  • 接下来会触发 #content1:target{ } 这条 CSS 规则,#content1 元素由 display:none 变成 display:block ,点击列表 2 亦是如此

如此即达到了 Tab 切换,当然除了 content1 content2 的切换,我们的 li 元素样式也要不断变化,这个时候,就需要我们在 DOM 结构布局的时候多留心,在 #content1:target 触发的时候可以同时去修改 li 的样式

在上面 HTML 的基础上,再修改一下,变成如下结构

1
2
3
4
5
6
<div id="content1">列表1内容:123456</div>
<div id="content2">列表2内容:abcdefgkijkl</div>
<ul class="nav">
<li><a href="#content1">列表1</a></li>
<li><a href="#content2">列表2</a></li>
</ul>

仔细对比一下与上面结构的异同,这里我只是将 ul 从两个 content 上面挪到了下面,为什么要这样做呢

EF{ cssRules } ,CSS3 兄弟选择符(EF) ,选择 E 元素后面的所有兄弟元素 F。

注意这里,最重要的一句话是 E~F 只能选择 E 元素 之后 的 F 元素,所以顺序就显得很重要了

在这样调换位置之后,通过兄弟选择符 ~ 可以操作整个 .nav 的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#content1:target ~ .nav li {
// 改变li元素的背景色和字体颜色
&:first-child {
background: #ff7300;
color: #fff;
}
}

#content2:target ~ .nav li {
// 改变li元素的背景色和字体颜色
&:last-child {
background: #ff7300;
color: #fff;
}
}

上面的 CSS 规则中,我们使用 ~ 选择符,在 #content1:target 和 #content2:target 触发的时候分别去控制两个导航 li 元素的样式,至此两个问题,1. 如何接收点击事件 与 2. 如何操作相关 DOM 都已经解决,剩下的是一些小样式的修补工作

&&

上面的方法通过添加 标签添加页面锚点的方式接收点击事件。
这里还有一种方式能够接收到点击事件,就是拥有 checked 属性的表单元素, 或者

前段时间在项目中遇到文件需要下载,a标签发现在下载jpg、txt文件时,并不会直接下载,而是会在浏览器中打开文件,即使给a标签添加了download属性,也无济于事

1
<a href="static/img/1.jpg" download="2.jpg">下载</a>

实现v-down(支持Jpg、Txt、Exact、Word、Pdf)

Vue页面中使用指令v-down

1
<span v-down="item.Url">(附件)</span>

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let baseDownloadUrl = "https://xxxxx.cn"; // 域名
// 添加自定义v-down指令
Vue.directive("down", {
inserted: (el, binding) => {
el.style.cssText = "cursor: pointer;color:red;";
el.addEventListener("click", () => {
console.log(binding.value);
let link = document.createElement("a"); // 创建a标签
link.style.display = "none";
link.href = baseDownloadUrl + binding.value; // 设置下载地址
link.setAttribute("download", ""); // 添加downLoad属性
document.body.appendChild(link);
link.click();
});
},
});

当下载文件中存在jpg、txt等浏览器可以直接预览的文件时,上面的方法就会出现问题,即使加了download仍然会在浏览器中打开下载文件,我们可以将下载地址借助Blob转换成二进制,然后作为a标签的href属性,配合download属性,实现下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Vue.directive("down", {
inserted: (el, binding) => {
el.style.cssText = "cursor: pointer;color:write;";
el.addEventListener("click", () => {
console.log(binding.value);
let link = document.createElement("a");
let url = baseDownloadUrl + binding.value;
// 这里是将url转成blob地址,
fetch(url)
.then((res) => res.blob())
.then((blob) => {
// 将链接地址字符内容转变成blob地址
link.href = URL.createObjectURL(blob);
console.log(link.href);
link.download = "";
document.body.appendChild(link);
link.click();
});
});
},
});

上面的这种方法,将地址进行了转化成blob,从而实现对jpg、txt 文件的下载

使用axios下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.$axios({
method: 'get',
url: '/task-common-api/api/v1/task/whitelist/template',
responseType: "blob"
})
.then(function (response) {
console.log(response);
if(response.data.type == "application/octet-stream") {
const filename = response.headers["content-disposition"];
const blob = new Blob([response.data]);
var downloadElement = document.createElement("a");
var href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = filename.split("filename=")[1];
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
}
})
.catch(function (error) {
console.log(error);
});

项目准备

服务端

新建app.js 文件 npm init -y初始化项目后,安装node-media-server

1
npm insatll node-media-server --save

app.js 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const nodeMediaServer = require("node-media-server");

const config = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 60,
ping_timeout: 30,
},
http: {
port: 8000,
allow_origin: "*",
},
};

var nms = new nodeMediaServer(config);
nms.run();

使用 node app.js 启动项目后即可进行推流了,我使用的是OBS

前端

使用flv.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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>直播</title>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.5.0/flv.min.js"></script>
<video id="videoElement" width="100%" controls></video>
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById("videoElement");
var flvPlayer = flvjs.createPlayer({
type: "flv",
url: "http://localhost:8000/live/1234.flv", //可以使用ws协议 ws://localhost:8000/live/1234.flv
// 1234 为串流密钥
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
</body>
</html>

路由拦截

首先在定义路由的时候就需要多添加一个自定义字段 requireAuth,用于判断该路由的访问是否需要登录。如果用户已经登录,则顺利进入路由, 否则就进入登录页面

1
2
3
4
5
6
7
8
9
10
11
12
const routes = [
{ path: "/", name: "/", component: Index },
{
path: "/repository",
name: "repository",
meta: {
requireAuth: true, // 添加该字段,表示进入这个路由是需要登录的
},
component: Repository,
},
{ path: "/login", name: "login", component: Login },
];

定义完路由后,我们主要是利用 vue-router 提供的钩子函数 beforeEach()对路由进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) {
// 判断该路由是否需要登录权限
if (store.state.token) {
// 通过vuex state获取当前的token是否存在
next();
} else {
next({
path: "/login",
query: { redirect: to.fullPath }, // 将跳转的路由path作为参数,登录成功后跳转到当前路由
});
}
} else {
next();
}
});

to.meta 中是我们自定义的数据,其中就包括我们刚刚定义的 requireAuth 字段,通过这个字段来判断该路由是否需要登录权限。需要的话,同时当前应用不存在 token,则跳转到登录页面,进行登录。登录成功后跳转到目标路由这种方式只是简单的前端路由控制,并不能真正阻止用户访问需要登录权限的路由还有一种情况便是:当前 token 失效了,但是 token 依然保存在本地。这时候你去访问需要登录权限的路由时,实际上应该让用户重新登录, 这时候就需要结合 http 拦截器 + 后端接口返回的 http 状态码来判断

Axios 拦截器

要想统一处理所有 http 请求和响应,就得用上 axios 的拦截器,通过配置 http response inteceptor,当后端接口返回 401 Unauthorized(未授权), 让用户重新登录

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
// http request 拦截器
axios.interceptors.request.use(
(config) => {
if (store.state.token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.Authorization = `token ${store.state.token}`;
}
return config;
},
(err) => {
return Promise.reject(err);
}
);

// http response 拦截器
axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response) {
switch (error.response.status) {
case 401:
// 返回 401 清除token信息并跳转到登录页面
store.commit(types.LOGOUT);
router.replace({
path: "login",
query: { redirect: router.currentRoute.fullPath },
});
}
}
return Promise.reject(error.response.data); // 返回接口返回的错误信息
}
);

通过上面这两步,就可以在前端实现登录拦截了,登出功能也就很简单,只需要把当前 token 清除,再跳转到首页即可

adb连接设备

开启/关闭 adb 服务

adb start-server (一般无需手动执行此命令,在运行 adb 命令时若发现 adb server 没有启动会自动调起)

adb kill-server

查看版本

adb version

查看设备

adb devices

通过 WIFI 进行远程连接调式

需要手机与本地电脑连接同一局域网

  1. 指定 adb 远程调试端口(默认端口 5555)

    // 使用adb开启(需要先使用usb数据线先连接)

    adb tcpip [port]

    // 以root权限开启
    setprop service.adb.tcp.port 5555

    stop adbd

    start adbd

  2. 连接

    adb connect [ip]:[port]

  3. 断开

    adb disconnect [ip]

基本使用

以 root 权限运行 adbd

adb root

安装应用

adb install [-lrtsdg] [path_to_apk]

参数 含义
-l 将应用安装到保护目录 /mnt/asec
-r 允许覆盖安装
-t 允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
-s 将应用安装到 sdcard
-d 允许降级覆盖安装
-g 授予所有运行时权限

卸载应用

adb uninstall [-k] [packagename]

packagename 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录

清除应用数据与缓存

adb shell pm clear [packagename]

详细文档

awesome-adb

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

0%