本站 - 私密文章模块原创
笔记
本站实现了私密文章功能,当大家想要「云端备份」到博客时,又不希望别人看到,该功能能满足你。
2022-01-07 @Young Kbt
# 前言
目前适用版本是 Vdoing v1.x。
本功能不是插件,好处在于你可以自定义喜欢的页面。
如果你想先体验私密文章的效果,请访问:
本模块分为四步:
- 创建 Login.vue 组件
- 创建一个 markdown 文档,引用 Login.vue 组件
- 监听路由,跳转前判断是否为私有文章,是否登录过,或是否登录状态过期
- 在 themeConfig 里添加一些配置信息
本模块功能:
- 网站验证功能:用于封锁整个网站,当第一次访问网站,需要进行验证登录,支持多组用户名和密码
- 私密文章验证功能:访问一篇文章时要进行验证登录,支持一篇私密文章多组用户名和密码
- 管理员验证功能:以管理员进行验证成功,网站和所有私密文章无需验证,直接访问
- 有效时间功能:验证成功后,在有效时间内的访问都不需要验证,支持天和小时为单位
# 组件添加
建议:本内容代码块比较长,可以点击代码块的右侧箭头来折叠,然后点击复制图标进行复制即可。
首先在 .vuepress/config.js 的 head 模块添加在线图标。图标库来自阿里云:https://www.iconfont.cn/
。
如果你没有账号,或者觉得添加比较麻烦,就使用我的图标库地址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。
['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3129839_xft6cqs5gc.css' }], // 阿里在线图标
在 doc/.vuepress 目录下,创建 components 文件夹,如果有,则不需要创建。
接着在 components 文件夹下创建 Login.vue
组件,该组件是登录的表单,可以根据需求自行修改。
一定是 components 文件夹且路径要对,因为 Vuepress 会自动全局注册该文件夹下的所有 .vue 组件。
在 Login.vue 添加如下内容:
<template>
<div class="login-form">
<div class="form-header">用户名</div>
<div>
<input
type="text"
class="form-control"
placeholder="请输入用户名 ..."
v-model="username"
/>
</div>
<div class="form-header">密码</div>
<div>
<input
type="password"
class="form-control"
placeholder="请输入密码 ..."
v-model="password"
/>
</div>
<div class="btn-row">
<button class="btn" @click="login">登录</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
username: "",
password: "",
privateInfo: {
username: "",
password: "",
loginKey: "",
expire: "",
loginInfo: "",
allLoginKey: "kbt",
},
};
},
mounted() {
// Enter 键也能触发登录按钮
document.onkeyup = (e) => {
let key = window.event.keyCode;
if (key == 13) {
this.login();
}
};
},
methods: {
/**
* 登录验证
*/
login() {
let { privateInfo } = this;
// 获取全局配置
let { username, password, loginKey, expire, firstLoginKey, loginInfo } =
this.$themeConfig.privatePage;
!loginKey && (loginKey = "vdoing_manager"); // 默认为 vdoing_manager
// 计算正确的过期时间
expire = this.getExpire(expire);
!expire && (expire = 86400000);
if (this.username && this.password) {
// 进入网站前进行验证
if (this.$route.query.verifyMode == "first") {
privateInfo.expire = expire;
!firstLoginKey && (firstLoginKey = "vdoing_first_login"); // 默认为 vdoing_first_login
// 检查 loginInfo 是否验证成功
let check = false;
if (loginInfo && loginInfo.hasOwnProperty(firstLoginKey)) {
check = this.checkLoginInfoAndJump(
loginInfo[firstLoginKey],
firstLoginKey
);
}
// 如果第一次进入网站以管理员登录,则网站的所有私密文章不再需要验证
if (
!check &&
this.username == username &&
this.password == password
) {
// 如果管理员登录,直接 key = vdoing_manager,不需要再次 key = vdoing_first_login
// this.storageLocalAndJump(firstLoginKey, false);
this.storageLocalAndJump(loginKey, true);
} else if (!check) {
this.password = ""; // 清空密码
addTip(
"用户名或者密码错误!请联系博主获取用户名和密码!",
"danger"
);
}
} else {
// 如果是单个文章验证
if (this.$route.query.verifyMode == "single") {
try {
this.$filterPosts.forEach((item) => {
if (item.path == this.$route.query.toPath) {
privateInfo.username = item.frontmatter.username;
privateInfo.password = item.frontmatter.password;
privateInfo.loginKey = item.frontmatter.permalink;
privateInfo.expire =
this.getExpire(item.frontmatter.expire) || expire;
privateInfo.loginInfo = item.frontmatter.loginInfo;
// 利用异常机制跳出 forEach 循环,break、return、continue 不会起作用
throw new Error();
}
});
} catch (e) {}
}
// checkLoginInfo:判断是否进行了 loginInfo 验证
let checkLoginInfo = false;
// 如果没有配置单私密文章用户信息,则使用全局配置
if (
!privateInfo.username &&
!privateInfo.password &&
!privateInfo.loginInfo
) {
privateInfo.loginKey = this.$route.query.toPath;
privateInfo.loginInfo = loginInfo;
privateInfo.expire ? "" : (privateInfo.expire = expire);
}
// 先进行 loginInfo 验证
if (privateInfo.loginInfo) {
// 如果是数组:即单个文章设置的 loginInfo
if (Array.isArray(privateInfo.loginInfo)) {
checkLoginInfo = this.checkLoginInfoAndJump(
privateInfo.loginInfo
);
} else if (
privateInfo.loginInfo.hasOwnProperty(this.$route.query.toPath)
) {
// 如果是对象,即全局设置的 loginInfo
checkLoginInfo = this.checkLoginInfoAndJump(
privateInfo.loginInfo[this.$route.query.toPath]
);
}
}
// 如果没有触发 loginInfo 验证或者 loginInfo 验证失败,则进行单个用户名密码验证
if (!checkLoginInfo) {
// 如果使用文章配置的用户名密码
if (
this.username == privateInfo.username &&
this.password == privateInfo.password
) {
this.storageLocalAndJump(this.privateInfo.loginKey, true);
} else if (
// 如果使用全局配置的用户名密码
this.username == username &&
this.password == password
) {
this.storageLocalAndJump(loginKey, true);
} else {
this.password = ""; // 清空密码
addTip(
"用户名或者密码错误!请联系博主获取用户名和密码!",
"danger"
);
}
}
}
} else if (this.username == "" && this.password != "") {
addTip("用户名不能为空!", "warning");
} else if (this.username != "" && this.password == "") {
addTip("密码不能为空!", "warning");
} else {
addTip("您访问的文章是私密文章,请先输入用户名和密码!", "info");
}
},
/**
* 检查 loginInfo 里的用户名和密码
* 匹配成功返回 true,失败返回 false
*/
checkLoginInfoAndJump(
loginInfo = this.privateInfo.loginInfo,
loginKey = this.privateInfo.loginKey
) {
try {
loginInfo.forEach((item) => {
if (
this.username == item.username &&
this.password == item.password
) {
this.storageLocalAndJump(loginKey, true);
// 利用异常机制跳出 forEach 循环,break、return、continue 不会起作用
throw new Error();
}
});
} catch (error) {
return true;
}
return false;
},
/**
* 添加登录信息到本地存储区,并跳转到私密文章
* loginKey:存储到本地的 key,方便自动验证
* jump:是否跳转到私密文章,默认存储到本地后跳转
*/
storageLocalAndJump(loginKey = this.privateInfo.loginKey, jump = true) {
const data = JSON.stringify({
username: this.username,
password: this.password,
time: new Date().getTime(),
expire: this.privateInfo.expire,
});
window.localStorage.setItem(loginKey, data);
if (jump) {
addTip("登录成功,正在跳转 ...", "success");
if (this.$route.query.toPath) {
this.$router.push({
path: this.$route.query.toPath,
});
} else {
this.$router.push({
path: "/",
});
}
}
},
/**
* 计算过期时间
*/
getExpire(expire) {
if (expire) {
if (expire.indexOf("d") !== -1) {
expire = parseInt(expire.replace("d", "")) * 24 * 60 * 60 * 1000; // 天
} else if (expire.indexOf("h") !== -1) {
expire = parseInt(expire.replace("h", "")) * 60 * 60 * 1000; // 小时
} else {
expire = parseInt(expire) * 1000; // 不加单位为秒
}
}
return expire;
},
},
};
/**
* 添加消息提示
* content:内容
* type:弹窗类型(tip、success、warning、danger)
* startHeight:第一个弹窗的高度,默认 50
* dieTime:弹窗消失时间(毫秒),默认 3000 毫秒
*/
function addTip(content, type, startHeight = 50, dieTime = 3000) {
var tip = document.querySelectorAll(".global-tip");
var time = new Date().getTime();
// 获取最后消息提示元素的高度
var top = tip.length == 0 ? 0 : tip[tip.length - 1].getAttribute("data-top");
// 如果产生两个以上的消息提示,则出现在上一个提示的下面,即高度添加,否则默认 50
var lastTop =
parseInt(top) +
(tip.length != 0 ? tip[tip.length - 1].offsetHeight + 17 : startHeight);
let div = document.createElement("div");
div.className = `global-tip tip-${type} ${time}`;
div.style.top = parseInt(top) + "px";
div.setAttribute("data-top", lastTop);
if (type == "info" || type == 1) {
div.innerHTML = `<i class="iconfont icon-info icon"></i><p class="tip-info-content">${content}</p>`;
} else if (type == "success" || type == 2) {
div.innerHTML = `<i class="iconfont icon-dagouyouquan icon"></i><p class="tip-success-content">${content}</p>`;
} else if (type == "danger" || type == 3) {
div.innerHTML = `<i class="iconfont icon-cuowu icon"></i><p class="tip-danger-content">${content}</p>`;
} else if (type == "warning" || type == 4) {
div.innerHTML = `<i class="iconfont icon-gantanhao icon"></i><p class="tip-warning-content">${content}</p>`;
}
document.body.appendChild(div);
let timeTip = document.getElementsByClassName(time)[0];
setTimeout(() => {
timeTip.style.top = parseInt(lastTop) + "px";
timeTip.style.opacity = "1";
}, 10);
// 消息提示 dieTime 秒后隐藏并被删除
setTimeout(() => {
timeTip.style.top = "0px";
timeTip.style.opacity = "0";
// 下面的所有元素回到各自曾经的出发点
var allTipElement = nextAllTipElement(timeTip);
for (let i = 0; i < allTipElement.length; i++) {
var next = allTipElement[i];
var top =
parseInt(next.getAttribute("data-top")) - next.offsetHeight - 17;
next.setAttribute("data-top", top);
next.style.top = top + "px";
}
setTimeout(() => {
timeTip.remove();
}, 500);
}, dieTime);
}
/**
* 获取后面的兄弟元素
*/
function nextAllTipElement(elem) {
var r = [];
var n = elem;
for (; n; n = n.nextSibling) {
if (n.nodeType === 1 && n !== elem) {
r.push(n);
}
}
return r;
}
</script>
<style lang="stylus">
.login-form {
padding: 1rem;
box-sizing: border-box;
.btn-row {
margin-top: 1rem;
text-align: center;
}
.btn {
padding: 0.6rem 2rem;
outline: none;
background-color: #60C084;
color: white;
border: 0;
cursor: pointer;
}
.form-header {
color: #13b9e2;
margin-bottom: 0.5rem;
}
.form-control {
padding: 0.6rem;
border: 2px solid #ddd;
width: 100%;
margin-bottom: 0.5rem;
box-sizing: border-box;
outline: none;
transition: border 0.2s ease;
&:focus {
border: 2px solid #aaa;
}
}
}
div.v-dialog-overlay {
opacity: 1 !important;
}
.global-tip {
position: fixed;
display: flex;
top: -10px;
left: 50%;
opacity: 0;
min-width: 320px;
transform: translateX(-50%);
transition: opacity 0.3s linear, top 0.4s, transform 0.4s;
z-index: 99999;
padding: 15px 15px 15px 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
grid-row: 1;
line-height: 17px;
}
.global-tip p {
line-height: 17px;
margin: 0;
font-size: 14px;
}
.icon {
margin-right: 10px;
line-height: 17px;
}
.tip-success {
color: #67c23a;
background-color: #f0f9eb;
border-color: #e1f3d8;
}
.tip-success .tip-success-content {
color: #67c23a;
}
.tip-danger {
color: #f56c6c;
background-color: #fef0f0;
border-color: #fde2e2;
}
.tip-danger .tip-danger-content {
color: #f56c6c;
}
.tip-info {
background-color: #edf2fc;
border-color: #ebeef5;
}
.tip-info .tip-info-content {
color: #909399;
}
.tip-warning {
color: #e6a23c;
background-color: #fdf6ec;
border-color: #faecd8;
}
.tip-warning .tip-warning-content {
margin: 0;
color: #e6a23c;
line-height: 21px;
font-size: 14px;
}
</style>
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# 组件引用
Login.vue 文件写好后需要引用,在 docs 目录下 的任意位置创建一个 markdown 文档,如我就在 docs 的根目录下创建 99.Vdoing私密文章登录.md
文件。
添加如下内容:(需要修改 frontmatter 为自己的内容)
---
title: Vdoing私密文章登录 # 可修改
date: 2022-01-07 14:26:04 # 你的创建时间,可修改
permalink: /vdoing/login/ # 可修改,建议按步骤使用,后面用到这个 permalink,否则要改一起改
sidebar: false
article: false
comment: false
editLink: false
---
您当前访问的是博主的私密文章,请输入有效的用户名和密码。如果没有,请在评论区或者其他途径向博主获取。
<ClientOnly>
<Login/>
</ClientOnly>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
记住你的 permalink
,后面的 配置添加 需要用到。
<ClientOnly>
大部分情况下可加可不加,少部分情况的官方介绍:https://v2.vuepress.vuejs.org/zh/reference/components.html#clientonly
。
# 路由监听
打开 docs/enhanceApp.js 文件,添加如下内容:
export default ({
Vue, // VuePress 正在使用的 Vue 构造函数
options, // 附加到根实例的一些选项
router, // 当前应用的路由实例
siteData, // 站点元数据
isServer // 当前应用配置是处于 服务端渲染 或 客户端
}) => {
/**
* 私密文章验证
*/
if (!isServer) {
// 如果开启了私密文章验证
if (
siteData.themeConfig.privatePage &&
siteData.themeConfig.privatePage.openPrivate
) {
router.beforeEach((to, from, next) => {
try {
let {
username,
password,
loginPath,
loginKey,
loginSession,
loginInfo,
firstLogin,
firstLoginKey,
} = siteData.themeConfig.privatePage;
!loginKey && (loginKey = "vdoing_manager"); // 默认为 vdoing_manager
!firstLoginKey && (firstLoginKey = "vdoing_first_login"); // 默认为 vdoing_first_login
// 网站关闭或者刷新后,清除登录状态
if (loginSession) {
window.addEventListener("unload", function () {
localStorage.removeItem(loginKey);
localStorage.removeItem(firstLoginKey);
});
}
// 如果是登录页面,不需要验证
if (loginPath == to.path || !loginPath) {
throw new Error("无需验证");
}
// 尝试获取管理员曾经登录的用户信息
let globalInfo = JSON.parse(localStorage.getItem(loginKey));
// 管理员用户名密码验证
if (
globalInfo &&
globalInfo.username == username &&
globalInfo.password == password
) {
// 存在曾经登录信息,如果登录状态过期
if (new Date() - globalInfo.time > globalInfo.expire) {
localStorage.removeItem(loginKey);
} else {
throw new Error("管理员验证成功!");
}
}
// 整个网站进入前需要验证
let isAgainLogin = true;
if (parseInt(firstLogin) == 1 || parseInt(firstLogin) == 2) {
parseInt(firstLogin) == 2 && (isAgainLogin = false);
// 尝试获取第一次访问网站曾经登录的用户信息
let firstLoginInfo = JSON.parse(
localStorage.getItem(firstLoginKey)
);
!firstLoginInfo && jumpToLogin(loginPath, to.path, "first");
if (firstLoginInfo) {
// 先判断 loginInfo 是否存在,然后判断 loginInfo 是否对象,最后判断 loginInfo 是否有 firstLoginKey
if (loginInfo && loginInfo.hasOwnProperty(firstLoginKey)) {
// 进行 loginInfo 验证
checkLoginInfo(loginInfo[firstLoginKey], firstLoginInfo) &&
jumpToLogin(loginPath, to.path, "first");
} else {
jumpToLogin(loginPath, to.path, "first");
}
}
}
if (to.path == "/") {
throw new Error("首页不需要验证!");
}
// 如果 firstLogin 不等于 2
if (isAgainLogin) {
siteData.pages.forEach((item) => {
// 找出带有 private 的文章
if (item.path == to.path) {
if (
item.frontmatter.private &&
item.frontmatter.private == true
) {
// 网站关闭或者刷新后,清除登录状态
if (loginSession) {
window.addEventListener("unload", function () {
localStorage.removeItem(item.frontmatter.permalink);
});
}
// 尝试获取该私密文章曾经登录的用户信息
let singleInfo = JSON.parse(
localStorage.getItem(item.frontmatter.permalink)
);
// 都不存在登录信息
!singleInfo &&
jumpToLogin(
loginPath,
to.path,
item.frontmatter.loginInfo ||
item.frontmatter.username ||
item.frontmatter.password ||
item.frontmatter.expire
? "single"
: "all"
);
// 单个文章私密验证
if (
(item.frontmatter.username && item.frontmatter.password) ||
item.frontmatter.loginInfo
) {
// 不存在登录信息,则跳转到登录页面
!singleInfo && jumpToLogin(loginPath, to.path, "single");
// 存在曾经登录信息,如果登录状态过期
if (new Date() - singleInfo.time > singleInfo.expire) {
localStorage.removeItem(item.frontmatter.permalink);
jumpToLogin(loginPath, to.path, "single");
}
// 是否需要登录
let isLogin = true;
// 对 loginInfo 进行验证
if (Array.isArray(item.frontmatter.loginInfo)) {
isLogin = checkLoginInfo(
item.frontmatter.loginInfo,
singleInfo
);
}
// 如果 loginInfo 不存在,则进行单文章的用户名密码验证
if (
isLogin &&
singleInfo.username !== item.frontmatter.username &&
singleInfo.password !== item.frontmatter.password
) {
jumpToLogin(loginPath, to.path, "single");
}
} else {
// 全局私密验证
let isLogin = true;
// 先判断 loginInfo 是否存在,然后判断 loginInfo 是否对象,最后判断 loginInfo 是否有该文章的 permalink
if (loginInfo && loginInfo.hasOwnProperty(to.path)) {
isLogin = checkLoginInfo(loginInfo[to.path], singleInfo);
}
// 如果 loginInfo 验证失败
isLogin && jumpToLogin(loginPath, to.path, "all");
}
}
}
});
}
} catch (e) {}
next();
});
}
}
/**
* 检查 loginInfo 里的用户名和密码,userInfo 为曾经登录的信息
* 匹配成功返回 false,失败返回 true
*/
function checkLoginInfo(loginInfo, userInfo) {
try {
loginInfo.forEach((info) => {
if (
userInfo.username == info.username &&
userInfo.password == info.password
) {
// 利用异常机制跳出 forEach 循环,break、return、continue 不会起作用
throw new Error();
}
});
} catch (error) {
return false;
}
return true;
}
/**
* 跳转到登录页面
* loginPath:登录页面的 permalink
* toPath:当前页面的 permalink,verifyMode:验证方式
*/
function jumpToLogin(loginPath, toPath, verifyMode) {
router.push({
path: loginPath,
query: {
toPath: toPath,
verifyMode: verifyMode, // 单个文章验证(single)或全局验证(all)或网站验证(first)
},
});
throw new Error("请先登录!");
}
}
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# 安全检测代码
因为 Vuepress 是静态页面,所以我们无法往后端获取登录信息,那么也就有一个问题,如果用户禁用 JavaScript,那么私有文章将不会进行验证,也就是禁用了 JavaScript,可以毫无阻塞的浏览私有文章内容,那么如何处理这个问题呢?
打开 .vuepress/config.js(新版是 config.ts)文件,给 head 模块添加如下信息:
['noscript', {}, '<meta http-equiv="refresh" content="0; url=https://www.youngkbt.cn/noscript/"><style>.theme-vdoing-content { display:none }']
值得注意的是,url
不要填写自己博客的任意地址,而是填写博客以外的地址,因为博客的页面总会触发这段代码,导致反复跳转该页面。
如果你不介意的话,可以用我提供的 url
,使用前你可以访问看看,只是一个简单的 html,点击跳转 (opens new window)。
# 配置添加
打开 .vuepress/config.js(新版为 config.ts) 文件,在 themeConfig 模块里添加如下内容:
// 私密文章配置
privatePage: {
openPrivate: true, // 开启私密文章验证,默认开启(true),如果不开启(false),则下面配置都失效
username: "youngkbt", // 管理员用户名
password: "123456", // 管理员密码
expire: "1d", // 登录过期时间:1d 代表 1 天,1h 代表 1 小时,仅支持这两个单位,不加单位代表秒。过期后访问私密文章重新输入用户名和密码。默认一天
loginPath: "/vdoing/login/", // 引用登录组件的 md 文章的 permalink(必须),无默认值
loginKey: "vdoing_manager", // 存储用户名信息的 key,默认是 vdoing_manager。系统通过该 key 验证是否登录、是否过期
loginSession: false, // 开启是否在网页关闭或刷新后,清除登录状态,这样再次访问网页,需要重新登录,默认为 false(不开启)
firstLogin: 0, // 第一次进入网站需要验证。用于封锁整个网站,默认为 0(不开启),1 和 2 都代表开启,区别:1 代表虽然进入网站成功,但是网站内的私密文章仍需要单独验证,2 代表进入网站成功,网站内的私密文章不需要单独验证,也就是网站内的私密文章和普通文章一样可以访问
firstLoginKey: "vdoing_first_login", // 存储用户名信息的 key,firstLogin 开启后该配置生效,默认为 vdoing_first_login,系统通过该 key 验证是否登录、是否过期
// 私密文章多组用户名密码
// loginInfo: {
// "/private/test1/": [
// { username: "vdoing", password: "123456" },
// ],
// "vdoing_first_login" :[ // 对应 firstLoginKey 的值
// { username: "vdoing", password: "123456" },
// ]
// }
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注释的内容是配置多组用户名密码,往下看。
如果您想封锁整个网站,进入网站前进行验证登录,请看 firstLogin 的配置介绍,它的功能也许对你有帮助。
# 配置介绍
openPrivate
- 类型:boolean
- 默认值:true
开启私密文章验证,默认开启(true),如果不开启(false),则所有私密文章的配置都失效。
为什么设计这个配置呢?如果你暂时不使用私密文章模块,但是又希望网站拥有私密文章模块,担心日后找不到本文章的地址,则可以先配置,然后改为 false 失效就行。
username
- 类型:
string
- 默认值:undefined
管理员用户名。
password
- 类型:
string
- 默认值:undefined
管理员密码。
expire
- 类型:
number
- 默认值:1d
登录过期时间:1d 代表 1 天,1h 代表 1 小时,仅支持这两个单位,不加单位代表秒。过期后访问私密文章重新输入用户名和密码。默认一天。
如果您想适配更多的单位,请自行修改 Login.vue 源码的 getExpire 方法(大约 225 - 236 处)。
loginPath
- 类型:
string
- 默认值:undefined
引用 Login.vue 组件的 markdown 文章中 frontmatter 的 permalink。
loginKey
- 类型:
string
- 默认值:vdoing_manager
存储用户名信息的 key。
请不要与任意文章中 frontmatter 的 permalink 冲突。
loginSession
- 类型:
boolean
- 默认值:false
是否开启在文章页面关闭或刷新后,清除登录状态。这样再次访问任何私密文章,都需要重新验证登录,默认为 false(不开启)。
firstLogin
- 类型:number
- 默认值:0
第一次进入网站需要验证。用于封锁整个网站。
默认为 0(不开启),1 和 2 都代表开启,区别:
- 1:进入网站验证成功,但是网站内的私密文章仍需要单独二次验证
- 2:进入网站验证成功,网站内的私密文章都不需要单独验证,也就是网站内的私密文章和普通文章一样可以正常访问
如果您不打算设置私密文章,即网站所有文章无需二次验证,只想 单纯封锁整个网站。则 强烈建议设置为 2,因为它能 降低验证性能。源码介绍如下:
- 如果 firstLogin 为 1,则每次进入新的文章都要进行拦截验证,判断是否为私密文章,可能损耗几毫秒到百毫秒(跟文章数有关)
- 如果 firstLogin 为 2,网站登录成功后,进入任意文章,都不会进行验证,减少了验证性能
firstLoginKey
- 类型:string
- 默认值:vdoing_first_login
存储用户名信息的 key,firstLogin 开启后该配置生效,系统通过该 key 验证是否登录、是否过期
loginInfo
- 类型:Object
- 默认值:undefined
配置私密文章多组用户名密码,key 为私密文章的 permalink,value 为数组,数组可以有多个用户名和密码。
# key介绍
在配置介绍中,有两个 key:loginKey
、firstLoginKey
,那么他们分别有什么作用呢?
您不希望每次进入网站或者访问私密文章都要进行验证吧,那么就需要一个有效时间,即在有效时间内,您的访问都能直接通过。
loginKey
和 firstLoginKey
就是为这个机制出现的,其实还有一个 key,所以总共有三个 key:
loginKey
为管理员服务,如果您以管理员的身份登录,那么系统就以 loginKey 存储管理员信息,有效时间内都不会进行验证firstLoginKey
为第一次访问网站服务,如果您登录成功了,那么该 key 就会存储您的登录信息,有效时间内都不会进行验证permalink
为私密文章服务,当您登录某篇私密文章后,那么该 key 就会存储您的登录信息,有效时间内都不会进行验证
permalink 内部能直接获取,所以不需要配置,您要确保的就是 这三个 key 不能都是同一个值。
# 开启私密文章
如果你想开启私密文章,请在 markdown 的 frontmatter 中 额外 添加如下内容:
---
private: true # 开启文章私密,必须
---
2
3
这是 最基本也是必须的步骤,开启了私密文章,还需要匹配对应的用户名和密码,看下面。
# 进入网站验证
如果您希望您的网站不暴露出去,可以使用该 网站验证功能。
那么配置 firstLogin 为 1 或者 2,则进入网站前需要验证,那么如何设置用户名和密码呢?
进入网站前的验证需要用到 firstLoginKey,然后在 loginInfo 里配置 firstLoginKey 的用户名和密码。
假设 firstLoginKey 为 vdoing_first_login
,则 loginInfo 里配置 vdoing_first_login
的用户名和密码,如下:
privatePage: {
// 其他配置
firstLogin: 1, // 或者 2
firstLoginKey: "vdoing_first_login",
loginInfo: {
"vdoing_first_login" :[ // 对应 firstLoginKey 的值
{ username: "vdoing1", password: "123" },
{ username: "vdoing2", password: "123456" },
]
},
},
2
3
4
5
6
7
8
9
10
11
这样,就能以 vdoing1、123
或者 vdoing2、123456
进行登录。
如果您修改了 firstLoginKey 的值,也请修改 loginInfo 里对应的值。
除了 loginInfo 里配置的用户名和密码登录,还可以进行管理员验证登录。
# 管理员验证
privatePage 里的 username 和 password 是管理员的登录信息。一旦登录管理员的账号,那么进入网站验证、网站的所有私密文章都无需验证,可以直接访问。
如何退出管理员的账号?
- 等待 expire 时间到期
- loginSession 为 true 时,只要关闭页面或者离开页面,就会清除所有登录信息,但是这个是针对所有用户,慎用
- 手动去浏览器的本地存储空间删除 loginKey 的密钥(这也是 loginSession 的原理)
如果 全局私密验证 或者 单私密文章验证 的用户名密码与管理员的一样,则以全局私密验证 或者 单私密文章验证 的用户名密码为主。
如果您并没有为私密文章配置登录信息(只设置 private: true
),则只能以管理员信息进行验证。
# 全局私密验证
privatePage 里的 loginInfo 可以指定私密文章的 permalink,然后配置多个用户名和密码,如下:
// 私密文章配置
privatePage: {
// 私密文章多组用户名密码
loginInfo: {
"/private1/": [ // 私密文章的 permalink
{ username: "vdoing1", password: "123" },
{ username: "vdoing2", password: "123456" }
],
"/private2/": [
{ username: "vdoing1", password: "123" },
{ username: "vdoing2", password: "123456" }
],
},
},
2
3
4
5
6
7
8
9
10
11
12
13
14
这样就能配置 permalink 为 /private1/
和 /private2/
私密文章的用户名和密码。
警告
如果打算使用该 loginInfo,则不能开启单私密文章验证,如果单私密文章指定了 username、password 或者 loginInfo,则该 loginInfo 失效。
2022.06.07 @Young Kbt
# 单私密文章验证
如果你想给某个私密文章设置单独的用户名和密码等配置,请在 frontmatter 中 额外 添加如下内容:
---
private: true # 开启文章私密,必须
username: vdoing # 用户名
password: 123456 # 密码
expire: 2d # 登录过期时间,可选(不填则以全局超时时间为准,如果全局也没有设置,则默认是一天)
loginInfo: [
{username: '1', password: '1'},
{username: '2', password: '2'},
{username: '3', password: '3'},
]
-
2
3
4
5
6
7
8
9
10
11
可以看到 frontmatter 出现了 username、password,并且 loginInfo 里也出现多个 username、password,怎么区分?
- 在登录方面没有任何区别。无论是以 username、password 登录,还是 loginInfo 里的多个 username、password 都可以
- 在快速配置方面有区别:如果您只是想给私密文章设置一个用户名和密码,则只配置 username、password 即可,不需要配置复杂的 loginInfo
警告
一旦在单私密文章开启 username 或 password 或 loginInfo,那么在全局的 loginInfo 的该文章用户名和密码不起效果,以单私密文章配置为主
如果您只想给单私密文章配置 expire 登录过期时间,这是可以的,只要单私密文章不出现 username 或 password 或 loginInfo 任意这三个配置,则都以全局的 loginInfo 为准
2022.06.07 @Young Kbt
# 举例
# 例1
假设一个私密文章的 frontmatter 如下:
---
title: 私密文章测试
date: 2022-01-07 17:01:37
permalink: /private1/
private: true
username: vdoing
password: 123456
expire: 7h
loginInfo: [
{username: '1', password: '1'},
]
---
2
3
4
5
6
7
8
9
10
11
12
全局配置如下:
privatePage: {
username: "yougnkbt", // 管理员用户名
password: "123", // 管理员密码
// ... 其他配置
// 私密文章多组用户名密码
loginInfo: {
"/private1/": [
{ username: "vdoing1", password: "123" },
],
},
},
2
3
4
5
6
7
8
9
10
11
如果登录时输入:
- 用户名:vdoing,密码:123456,登录成功
- 用户名:1,密码:1,登录成功
- 用户名:youngkbt,密码:123,登录成功,因为这是管理员的账号,对所有私密文章生效
- 用户名:vdoing1,密码:123,登录失败,因为出现了单私密文章配置,则以单私密文章配置为主
# 例2
假设一个私密文章的 frontmatter 如下:
---
title: 私密文章测试
date: 2022-01-07 17:01:37
permalink: /private1/
private: true
expire: 7h
---
2
3
4
5
6
7
全局配置如下:
privatePage: {
// ... 其他配置
// 私密文章多组用户名密码
loginInfo: {
"/private1/": [
{ username: "vdoing1", password: "123" },
],
"/privat2/": [
{ username: "vdoing2", password: "123456" },
],
},
},
2
3
4
5
6
7
8
9
10
11
12
如果登录时输入:
- 用户名:vdoing1,密码:123,登录成功,没单私密文章配置,则以全局私密文章配置为主
- 用户名:vdoing2,密码:123456,登录失败,
/private2/
并不是该私密文章的 permalink
虽然单私密文章出现了 expire: 7h
,但是没有用户登录信息,所以仍然以全局私密信息为主。
# 结束语
如果你的 Vdoing 项目使用了本模块,建议不要将项目公开出去,拿 Github 举例,可以去 GitHub 仓库查看你的用户名和密码,进行登录访问。
如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。
如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!