2024-10-05 08:00:00
数据无价,备份无忧。
将其他平台数据备份到不同平台的好处:
又拍云目录:
github目录:
upyun_images_sync/.github/workflows/sync-images.yml
name: Sync Images from UpYun to GitHub
on:
schedule:
- cron: '0 0 * * 3' # 每周三凌晨 0 点运行
workflow_dispatch: # 允许手动触发
jobs:
sync-images:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install gitpython
- name: Run sync script
env:
UPYUN_FTP_HOST: $
UPYUN_FTP_USER: $
UPYUN_FTP_PASSWORD: $
UPYUN_FTP_PATH: $
LOCAL_DOWNLOAD_PATH: ./tmp
REPO_PATH: ./repo
GH_TOKEN: $
REPO_OWNER: $
REPO_NAME: $
BRANCH_NAME: $
run: |
python3 sync_images.py
upyun_images_sync/sync_images.py
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
from ftplib import FTP
import git
import shutil
class MyFTP:
def __init__(self, host, port=21):
self.host = host
self.port = port
self.ftp = FTP()
self.ftp.encoding = 'utf8'
self.file_list = []
def login(self, username, password):
try:
self.ftp.connect(self.host, self.port)
self.ftp.login(username, password)
print(f"成功登录到 {self.host}")
print(f"当前工作目录: {self.ftp.pwd()}")
except Exception as err:
print(f"FTP 连接或登录失败,错误描述为:{err}")
sys.exit(1)
def download_file(self, local_file, remote_file):
if os.path.exists(local_file):
print(f"{local_file} 已存在,跳过下载")
return
try:
print(f"下载文件 {remote_file} 到 {local_file}")
buf_size = 1024
with open(local_file, 'wb') as file_handler:
self.ftp.retrbinary(f'RETR {remote_file}', file_handler.write, buf_size)
except Exception as err:
print(f"下载文件出错,出现异常:{err}")
def download_file_tree(self, local_path, remote_path):
try:
print(f"尝试切换到远程目录: {remote_path}")
self.ftp.cwd(remote_path)
print(f"成功切换到远程目录: {remote_path}")
except Exception as err:
print(f"远程目录 {remote_path} 不存在,继续... 具体错误描述为:{err}")
return
if not os.path.isdir(local_path):
os.makedirs(local_path)
print(f"创建本地目录 {local_path}")
print(f"当前工作目录: {self.ftp.pwd()}")
# 列出目录内容
remote_files = self.ftp.nlst()
print(f"远程目录内容: {remote_files}")
for remote_file in remote_files:
local = os.path.join(local_path, remote_file)
if remote_file in ['.', '..']:
continue
try:
self.ftp.cwd(remote_file)
print(f"下载目录:{remote_file}")
self.download_file_tree(local, os.path.join(remote_path, remote_file))
self.ftp.cwd("..")
except Exception:
print(f"下载文件:{remote_file}")
self.download_file(local, os.path.join(remote_path, remote_file))
def close(self):
print("FTP退出")
self.ftp.quit()
def sync_to_github(repo_path, branch_name):
if os.path.exists(repo_path):
shutil.rmtree(repo_path)
os.makedirs(repo_path)
repo_url = f"https://{GH_TOKEN}@github.com/{REPO_OWNER}/{REPO_NAME}.git"
print(f"克隆仓库: {repo_url}")
try:
repo = git.Repo.clone_from(repo_url, repo_path, branch=branch_name)
print(f"克隆仓库到 {repo_path}")
except git.exc.GitCommandError as e:
print(f"克隆仓库失败: {e}")
return
images_dir = os.path.join(repo_path, 'images')
if not os.path.exists(images_dir):
os.makedirs(images_dir)
shutil.copytree(LOCAL_DOWNLOAD_PATH, images_dir, dirs_exist_ok=True)
print(f"复制文件到 {images_dir}")
repo.git.add(all=True)
if repo.is_dirty():
repo.index.commit("Sync images from UpYun")
origin = repo.remote(name='origin')
origin.push()
print("推送更改到 GitHub 仓库")
else:
print("没有更改需要提交")
if __name__ == "__main__":
# 从环境变量中读取配置
UPYUN_FTP_HOST = os.getenv('UPYUN_FTP_HOST')
UPYUN_FTP_USER = os.getenv('UPYUN_FTP_USER')
UPYUN_FTP_PASSWORD = os.getenv('UPYUN_FTP_PASSWORD')
UPYUN_FTP_PATH = os.getenv('UPYUN_FTP_PATH', '/')
LOCAL_DOWNLOAD_PATH = os.getenv('LOCAL_DOWNLOAD_PATH', './tmp')
REPO_PATH = os.getenv('REPO_PATH', './repo')
GH_TOKEN = os.getenv('GH_TOKEN')
REPO_OWNER = os.getenv('REPO_OWNER')
REPO_NAME = os.getenv('REPO_NAME')
BRANCH_NAME = os.getenv('BRANCH_NAME', 'master')
# 创建 FTP 对象
my_ftp = MyFTP(UPYUN_FTP_HOST)
# 登录 FTP 服务器
my_ftp.login(UPYUN_FTP_USER, UPYUN_FTP_PASSWORD)
# 下载目录
my_ftp.download_file_tree(LOCAL_DOWNLOAD_PATH, UPYUN_FTP_PATH)
# 关闭 FTP 连接
my_ftp.close()
# 同步到 GitHub
sync_to_github(REPO_PATH, BRANCH_NAME)
将本地的项目提交到github,然后设置Workflow 读写权限
在github上设置Secrets 变量
序号 | 变量名 | 释义 |
---|---|---|
1 | UPYUN_FTP_HOST | 又拍云 FTP 主机地址 |
2 | UPYUN_FTP_USER | 又拍云 FTP 用户名 |
3 | UPYUN_FTP_PASSWORD | 又拍云 FTP 密码 |
4 | UPYUN_FTP_PATH | 又拍云 FTP 路径 |
5 | GH_TOKEN | GitHub 个人访问令牌 |
6 | REPO_OWNER | 仓库所有者 |
7 | REPO_NAME | 仓库名称 |
8 | BRANCH_NAME | 分支名称(默认为 master) |
变量详解:
UPYUN_FTP_HOST
:参考又拍云文档,直接使用v0.ftp.upyun.com
就行了UPYUN_FTP_USER
:格式为 operator/bucket,在又拍云 - 云存储 - 选择对应的bucket点击配置 - 存储管理 - 操作员授权 - 自己添加操作用户名和密码,假设你的又拍云云储存bucket的名称为upai-img,自定义的用户名为user,密码为123456,则UPYUN_FTP_USER就是 user/upai-img
,如果你看不懂我说的,参考 又拍云视频教程
UPYUN_FTP_PASSWORD
:就是上面你自己设置的 123456
,图片示例
UPYUN_FTP_PATH
:直接使用 /
(根目录)就行了GH_TOKEN
:选择github的 Settings - 点击右侧列表最下面的 Developer Settings - Personal access tokens - tokens (classic) - Generated new token (classic),Note名字可以随意,Expiration时间选择 no Expiration,下面权限全选了,点击Generated token 就生成了想要的token,记得保存,他只显示一次,示例:abc_123456789pUS123454321WmJkE987654321
,图片示例
REPO_OWNER
:就是你的github的用户名,我在github给这个项目创建仓库为https://github.com/gorpeln/upyun_images_sync,示例:gorpeln
REPO_NAME
:仓库链接后面的就是仓库名,示例:upyun_images_sync
,图片示例
BRANCH_NAME
:项目分支名,一般为master/main,示例:master
2024-10-05 08:00:00
2024-09-17 08:00:00
博客目标?
简洁、高效
如何联系博主?
可以采用邮件联系(请手动将邮件地址中的*替换掉),博主看到后会及时回复的。
评论加载不出来?
博客已开启CDN,整体访问速度较快。
但博客评论、公告、实验室等功能采用不同域名访问,可能存在无法访问的情况。
如果可能,请科学上网。或者多次刷新加载(5-6次),就可以正常展示。
博客成长记录?
友链相关问题?
博客相关协议?
其他问题?
正在整理中...
2024-08-24 08:00:00
网站镜像是指在互联网上出现一个与你的网站几乎一模一样的复制品,除了域名不同之外,其它所有内容都完全相同。这包括网站的布局、LOGO、版块结构等。
通常,网站被恶意镜像的情况主要有两种:
在上述例子中,明显属于第一种情况:对方镜像了我的网站布局和结构。这种行为属于灰色 SEO 和黑帽 SEO 手法,目的是为了借助我博客的权重和流量来提升他们自己网站的排名。
从搜索引擎来讲,会对搜索引擎抓取不利,影响原本网站的正常抓取和识别,可能会导致原网站权重丢失,也有可能带来一定的误伤,搜索引擎会对网站进行识别,如果发现是镜像站,会导致网站被搜索引擎屏蔽,将失去搜索引擎带来的流量(这对个人博客来说是致命的)。
对用户来讲,可能会被镜像网站欺骗,恶意广告插播或欺诈内容造成用户损失。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>not mirroring</title>
<style>
.alert-message {
position: fixed;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f8d7da;
color: #721c24;
padding: 20px;
z-index: 10000;
opacity: 1;
transition: opacity 3s;
border-radius:16px;
}
.alert-message.fade-out {
opacity: 0;
}
</style>
</head>
<body>
<script>
(function () {
// 定义合法域名列表并Base64编码
var validDomains = ['Z29ycGVsbi50b3A=']; // gorpeln.top
var redirectUrl = 'aHR0cHM6Ly9nb3JwZWxuLnRvcA=='; // https://gorpeln.top
var hostname = document.location.hostname + (document.location.port ? ':' + document.location.port : '');
function createWatermark(text) {
var watermarkDiv = document.createElement('div');
watermarkDiv.style.pointerEvents = 'none';
watermarkDiv.style.position = 'fixed';
watermarkDiv.style.top = '0';
watermarkDiv.style.left = '-5%';
watermarkDiv.style.width = '110%';
watermarkDiv.style.height = '100%';
watermarkDiv.style.zIndex = '9999';
watermarkDiv.style.opacity = '0.1';
watermarkDiv.style.background = 'transparent';
watermarkDiv.style.overflow = 'hidden';
watermarkDiv.style.display = 'flex';
watermarkDiv.style.justifyContent = 'center';
watermarkDiv.style.alignItems = 'center';
watermarkDiv.style.flexWrap = 'wrap';
var watermarkText = document.createElement('div');
watermarkText.innerText = text;
watermarkText.style.color = 'black';
watermarkText.style.fontSize = '30px';
watermarkText.style.transform = 'rotate(-30deg)';
watermarkText.style.whiteSpace = 'nowrap';
watermarkText.style.margin = '80px';
for (var i = 0; i < 100; i++) {
watermarkDiv.appendChild(watermarkText.cloneNode(true));
}
document.body.appendChild(watermarkDiv);
}
function showAlertMessage(message) {
var alertMessage = document.createElement('div');
alertMessage.className = 'alert-message';
alertMessage.innerText = message;
document.body.appendChild(alertMessage);
// 3秒后添加fade-out类,使消息淡出
setTimeout(function() {
alertMessage.classList.add('fade-out');
// 再等1秒(总共4秒)后删除元素
setTimeout(function() {
if (alertMessage.parentNode) {
alertMessage.parentNode.removeChild(alertMessage);
}
}, 200);
}, 3000);
// 5秒后跳转
setTimeout(function() {
window.location.replace(atob(redirectUrl));
}, 5000);
}
// 检查当前域名是否在合法域名列表中
if (!validDomains.includes(btoa(hostname))) {
createWatermark(atob(validDomains[0])); // 使用合法域名列表中的第一个元素
showAlertMessage("警告:你当前浏览的页面非官方页面,可能存在有害信息!将在5秒后为你跳转至官方页面进行浏览!");
}
})();
</script>
</body>
</html>
这个代码的目的是,利用对方会无脑反代一切内容的机制,在所有页面内都插入检测 JS 代码,在网友访问时,检测当前域名是否为所设定自己博客的域名,如果不是所设定的博客域名则在网站背景中嵌入带域名水印
并使用 confirm() 打断页面渲染,并弹出弹窗警示
用户自动跳转回源站
。
并且使用 JavaScript 动态插入水印 div 并不设置 id,class 等标识,防止对方通过u正则表达式匹配删除特定 div 元素。
因为 JavaScript 仍然是以明文方式暴露在 HTML 中,有正则表达式匹配风险,所以如果对方使用正则表达式破坏水印 JavaScript,则会导致水印无法正常显示。反制方法也很简单:使用 JavaScript 混淆。
这里推荐一个 GitHub 上一个项目:https://github.com/javascript-obfuscator/javascript-obfuscator,该项目可以对 JavaScript 进行混淆,官方也提供了一个在线工具:https://obfuscator.io/#code
我们可以借此对水印部分的 JavaScript 进行混淆,防止对水印 JavaScript 部分进行正则表达式匹配破坏,上面代码经过中等级混淆后如下:
function _0x1244(_0x2e9cd0,_0x16f71d){var _0x2ccdb5=_0x2ccd();return _0x1244=function(_0x124436,_0x222862){_0x124436=_0x124436-0x1d3;var _0x36d7fe=_0x2ccdb5[_0x124436];return _0x36d7fe;},_0x1244(_0x2e9cd0,_0x16f71d);}function _0x2ccd(){var _0x519334=['4159181SScBxf','center','110%','pointerEvents','80px','left','fixed','parentNode','117310NCrasO','width','33428dRtpro','wrap','hostname','fade-out','background','flex','30px','85638ogEylA','none','black','0.1','flexWrap','location','opacity','transform','10FNisEK','whiteSpace','position','body','100%','createElement','rotate(-30deg)','port','transparent','hidden','36vpXQzg','1078040hBJRkh','overflow','4510488GmCyEv','display','div','1243HhykLg','294OWGOMk','alert-message','-5%','margin','cloneNode','justifyContent','appendChild','alignItems','fontSize','zIndex','警告:你当前浏览的页面非官方页面,可能存在有害信息!将在5秒后为你跳转至官方页面进行浏览!','add','4592382EinlGm','36cjOBwx','innerText','removeChild','5neLlLf','style','includes'];_0x2ccd=function(){return _0x519334;};return _0x2ccd();}(function(_0x4df09c,_0x8f741a){var _0x8f2856=_0x1244,_0x22b1d8=_0x4df09c();while(!![]){try{var _0x5f3be6=-parseInt(_0x8f2856(0x1f2))/0x1*(parseInt(_0x8f2856(0x201))/0x2)+-parseInt(_0x8f2856(0x1e1))/0x3+-parseInt(_0x8f2856(0x20e))/0x4*(-parseInt(_0x8f2856(0x1e5))/0x5)+-parseInt(_0x8f2856(0x1f9))/0x6*(-parseInt(_0x8f2856(0x1d5))/0x7)+-parseInt(_0x8f2856(0x20c))/0x8*(-parseInt(_0x8f2856(0x20b))/0x9)+parseInt(_0x8f2856(0x1f0))/0xa*(parseInt(_0x8f2856(0x1d4))/0xb)+parseInt(_0x8f2856(0x1e2))/0xc*(-parseInt(_0x8f2856(0x1e8))/0xd);if(_0x5f3be6===_0x8f741a)break;else _0x22b1d8['push'](_0x22b1d8['shift']());}catch(_0x3801aa){_0x22b1d8['push'](_0x22b1d8['shift']());}}}(_0x2ccd,0xe404e),(function(){var _0x2a9a37=_0x1244,_0x3507a2=['Z29ycGVsbi50b3A='],_0x49805e='aHR0cHM6Ly9nb3JwZWxuLnRvcA==',_0x2e6736=document['location'][_0x2a9a37(0x1f4)]+(document[_0x2a9a37(0x1fe)][_0x2a9a37(0x208)]?':'+document['location']['port']:'');function _0xc49f33(_0x38600e){var _0x202c5e=_0x2a9a37,_0x2660f1=document['createElement']('div');_0x2660f1['style'][_0x202c5e(0x1eb)]=_0x202c5e(0x1fa),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x203)]=_0x202c5e(0x1ee),_0x2660f1['style']['top']='0',_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x1ed)]=_0x202c5e(0x1d7),_0x2660f1['style'][_0x202c5e(0x1f1)]=_0x202c5e(0x1ea),_0x2660f1['style']['height']=_0x202c5e(0x205),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x1de)]='9999',_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x1ff)]=_0x202c5e(0x1fc),_0x2660f1['style'][_0x202c5e(0x1f6)]=_0x202c5e(0x209),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x20d)]=_0x202c5e(0x20a),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x20f)]=_0x202c5e(0x1f7),_0x2660f1['style'][_0x202c5e(0x1da)]=_0x202c5e(0x1e9),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x1dc)]=_0x202c5e(0x1e9),_0x2660f1[_0x202c5e(0x1e6)][_0x202c5e(0x1fd)]=_0x202c5e(0x1f3);var _0x323b98=document[_0x202c5e(0x206)](_0x202c5e(0x1d3));_0x323b98[_0x202c5e(0x1e3)]=_0x38600e,_0x323b98[_0x202c5e(0x1e6)]['color']=_0x202c5e(0x1fb),_0x323b98[_0x202c5e(0x1e6)][_0x202c5e(0x1dd)]=_0x202c5e(0x1f8),_0x323b98[_0x202c5e(0x1e6)][_0x202c5e(0x200)]=_0x202c5e(0x207),_0x323b98['style'][_0x202c5e(0x202)]='nowrap',_0x323b98[_0x202c5e(0x1e6)][_0x202c5e(0x1d8)]=_0x202c5e(0x1ec);for(var _0x17c22c=0x0;_0x17c22c<0x64;_0x17c22c++){_0x2660f1[_0x202c5e(0x1db)](_0x323b98[_0x202c5e(0x1d9)](!![]));}document[_0x202c5e(0x204)][_0x202c5e(0x1db)](_0x2660f1);}function _0x1aa9d7(_0x4e583e){var _0x37bf9d=_0x2a9a37,_0x4d1163=document[_0x37bf9d(0x206)]('div');_0x4d1163['className']=_0x37bf9d(0x1d6),_0x4d1163[_0x37bf9d(0x1e3)]=_0x4e583e,document[_0x37bf9d(0x204)][_0x37bf9d(0x1db)](_0x4d1163),setTimeout(function(){var _0x3d022c=_0x37bf9d;_0x4d1163['classList'][_0x3d022c(0x1e0)](_0x3d022c(0x1f5)),setTimeout(function(){var _0x68fa28=_0x3d022c;_0x4d1163[_0x68fa28(0x1ef)]&&_0x4d1163[_0x68fa28(0x1ef)][_0x68fa28(0x1e4)](_0x4d1163);},0xc8);},0xbb8),setTimeout(function(){var _0x528155=_0x37bf9d;window[_0x528155(0x1fe)]['replace'](atob(_0x49805e));},0x1388);}!_0x3507a2[_0x2a9a37(0x1e7)](btoa(_0x2e6736))&&(_0xc49f33(atob(_0x3507a2[0x0])),_0x1aa9d7(_0x2a9a37(0x1df)));}()));
1、向 google、域名注册商 、域名解析商举报
2、设置 IP 黑名单
3、向谷歌申请移除镜像站的搜索结果
2024-07-06 08:00:00
Follow 是一款面向未来的信息浏览器,致力于将各种信息源整合在一个平台上,让用户便捷地获取和管理资讯。无论是传统的 RSS 订阅、社交媒体账号、博客、播客,还是实时通知,Follow 都能帮您一站式浏览。界面简洁,操作迅速,为用户提供现代化的阅读体验。
Follow的slogan是:Next generation information browser
Follow的主要特色:
信息一体化管理:Follow 支持广泛的信息源,除了传统的 RSS,还包括 Twitter、Instagram、YouTube 等社交平台,用户可以在一个界面中浏览全部订阅内容,方便直观。
智能化 AI 功能:内置的 AI 不仅能实现内容翻译和摘要,还能每日生成两次个性化报告,提炼重要信息,帮助用户轻松跟踪最关心的动态。此外,Follow 还能根据您的订阅喜好建立个性化知识库。
区块链激励机制:Follow 通过区块链技术为活跃用户和内容创作者提供动力代币奖励,鼓励用户积极参与和分享内容,提升整体使用体验。
社交功能:Follow 也具备社交平台属性,用户可以关注他人,分享订阅列表,发现新内容。支持与好友同步订阅列表,便捷地分享和发现优质资源。
Follow的另一个亮点是支持打赏功能,你可以给你喜欢的文章进行打赏,当然前提是这个文章所有者进行了Follow认证,打赏的金额才能进入到他的账户中去。Follow使用的打赏货币名为power,是一种区块链货币,按照官方的说法会根据在Follow上的活跃度和贡献值来获取power。
在Follow中订阅自己博客,然后点击订阅源,申请认证(Claim),便可获取认证码。如下
This message is used to verify that this feed (feedId:73252521066438656) belongs to me (userId:75520668589921280). Join me in enjoying the next generation information browser https://follow.is.
在你自己博客中发布一篇博文,内容为刚生成的认证码,然后在Follow中再次点击申请认证(Claim),即可完成认证。认证完成后认证码的相关博文信息就可以删除了。
目前gorpeln’s Blog已经过认证,在搜索框中直接输入『 gorpeln 』可直接订阅我的更新动态。或者直接填入gorpeln的RSS地址也是可以的 https://gorpeln.top/feed.xml
其他top订阅可以访问github开源项目top-rss-list
2024-06-29 08:00:00
我们往往会在不同的网站上使用相同的密码,这样一旦一个网站账户的密码泄露,就会危及到其他使用相同密码的账户的安全,这也是最近的密码泄露事件造成如此大影响的原因。为了解决这个问题,一些网站在登录时要求除了输入账户密码之外,还需要输入另一个一次性密码,这就是常说的多步验证(多因子认证)。
多因子认证(Multi-factor Authentication),简称MFA
。在现代网络安全领域,多因子认证已成为保护用户账户安全的重要手段,它要求用户提供两个或更多独立的认证因子,例如:账号密码+短信验证码、账号密码+邮箱验证码、账号密码+TOTP验证码等,当然对于Gtihub要求的2FA
,指的是Two-Factor Authentication双因子认证,与MFA的主要区别是认证因子的数量,从更广义的层面来看,2FA可以看作是MFA的一种特殊情况,即MFA的一个子集
MFA的主要目的是增强安全性。相比于单一的用户名和密码认证,MFA增加了额外的验证方式,从而显著降低了账户被入侵的风险。即便攻击者获得了账号的用户名和密码,仍需要其他认证因子才能访问账户,极大地提高了账户的安全性
常见的多因子认证方式有:
短信验证码
:这种验证方式非常广泛,在用户在登录时,系统会发送一个一次性验证码到用户的手机上,用户则需要在登录页面输入这个验证码来完成身份验证,这种认证方式的优点是简单方便,用户无需额外的设备或应用就能完成二次验证,缺点则主要是会产生短信费用,同时也可能会受到SIM卡交换攻击或短信拦截
邮件验证码
:系统发送一次性验证码到用户的电子邮箱,用户需要在登录页面输入该验证码完成验证,邮箱验证码使用也较为广泛,这种认证方式优点是易于实现和使用,用户无需额外设备,只要有邮箱即可,缺点则主要是邮件响应速度较慢,用户体验不佳,同时安全性较低,邮箱可能被黑客入侵
生物识别认证
:使用指纹、面部识别或声纹等生物特征进行身份验证,常用于高安全性需求场景,如解锁智能手机或访问敏感系统。这种认证方式优点是安全性极高,唯一性强、用户体验也好,快速便捷,缺点主要是设备要求高,需要设备支持指纹、面部解锁等,其次一些场景无法支持,例如WEB端,同时也有隐私问题需考虑
硬件令牌
:这种常见于财务系统,使用独立的硬件设备生成一次性密码或进行物理验证,用户将硬件令牌插入计算机或使用NFC读取器完成验证。优点是极高的安全性,同时也能防止网络攻击,因为它不依赖移动设备或互联网连接,缺点也比较明显,依赖硬件,成本比较高,使用不方便
基于时间的一次性密码
(TOTP):基于时间生成的一次性密码,有标准的算法,通过标准算法来生成验证码,有许多免费的应用或小程序都能生成,用户需要输入应用生成的验证码来完成身份验证。这种认证方式的主要优点是安全性高,不依赖网络传输,多数MFA应用免费提供,缺点则主要是用户需要额外安装并配置应用
基于事件的一次性密码
(HOTP):基于事件计数生成的一次性密码,类似于TOTP,但使用事件计数而非时间。适用于硬件令牌。这种认证方式的优点是不依赖时间同步,适用于硬件设备,缺点则主要是使用体验不如TOTP方便
推送通知
:通过认证应用发送推送通知到用户的设备,用户只需点击“批准”或“拒绝”即可完成身份验证,例如之前遇到过的Google账号认证,会发送消息到你登录Google账号的可信手机上,点击确定就能登录。这种认证方式的优点是:用户体验好,方便快捷、更加安全,防止中间人攻击,缺点则是需要互联网连接、依赖移动设备,也不是所有设备都支持
安全问题
:用户设置一系列安全问题,登录时需回答这些问题进行验证。这种认证方式的优点是实施简单,无需额外设备,缺点则主要是安全性较低,容易被猜测或社交工程攻击,长时间不使用还容易遗忘,例如我小时候申请的QQ号密保问题是:你的梦想是什么?现在已经完全想不起来答案了
其中,基于时间的一次性密码TOTP
是一种非常流行且标准化的多因子认证方式,尤其是在Web系统中,例如阿里云、腾讯云、Github、Google等诸多知名网站都支持TOTP多因子认证,另外一些应用较广的运维相关开源软件也都加入了对TOTP二次认证的支持,例如CODO、Jumpserver等,甚至出现了多因子认证约等于TOTP的现象
# T表示当前时间的unix时间戳,T0表示初始时间(一般为0,可省略)
# Period表示更新周期(一般为30秒)
# C表示基于时间生成的计数
C = (T - T0) / Period
# K:加密密码,作为HMAC密码算法输入,由于只有客户端和服务端共享,因此在不知道K的情况下,无法生成,保证安全性。
# C:计数器,客户端和服务端基于本地时间分别计算
# h:表示使用密码技术得到的一次密码(但是由于h长度较大,不适合作为验证码,因此需要进一步截取
h = HMAC(K, C)
# otp:一次密码,通过对h进行截取处理(这里的digit代表需要截取的位数,通常情况下为6)
otp = Trunc(h, digit)
TOTP
(Time-Based One-Time Password)算法使用的是基于时间的一次性密码,这是一种广泛应用于两步验证过程的算法。TOTP的工作原理可以概括如下:
TOTP的优点:
无需网络
:生成密码不依赖于网络连接,因为它基于时间和共享密钥,这增加了安全性和可用性举一个不恰当但简单易懂的例子:首先使用手机扫描平台的二维码从服务器获取秘钥(假如为123456),那么TOTP客户端就可以根据秘钥和当前时间(假设为2024年09月12日16时48分15秒)生成一个6位的动态密码,时间截取日时分121648,123456×121648=15018175488直接截取后六位175488,那么48分的动态密码就是175488,同样的49分时,123456×121649=15018298944,得到动态密码为298944,这样当时间发生变化时每分钟都可以生成不一样的密码,同样的服务器也使用相同的方法根据秘钥和时间生成对应的密码,当你登录平台时手动输入TOTP客户端提供的动态密码给服务器,服务器会将结果与自己生成的密码比对,比对结果一致就可以正常登录了。当然这个例子只是帮助你理解TOTP,实际的商用环境下,加密方式肯定比我举例的这种方式复杂,需要考虑更多的因素,如果感兴趣可以去学习了解详细的TOTP算法。
TOTP的算法是公开的,所以生成TOTP验证码也较为简单,国内各大云厂商的APP或是一些小程序也都有提供虚拟MFA的功能,例如腾讯云助手小程序等等,知名的MFA应用主要有Google验证器(Google Authenticator
)和微软验证器(Microsoft Authenticator)
以Github为例开启2FA。
1.时间T的值怎么选取?
因为时间每时每刻都在变化,如果选择一个变化太快的T(例如从某一时间点开始的秒数),那么用户来不及输入密码。如果选择一个变化太慢的T(例如从某一时间点开始的小时数),那么第三方攻击者就有充足的时间去尝试所有可能的一次性密码(试想6位数字的一次性密码仅仅有10^6种组合),降低了密码的安全性。除此之外,变化太慢的T还会导致另一个问题。如果用户需要在短时间内两次登录账户,由于密码是一次性的不可重用,用户必须等到下一个一次性密码被生成时才能登录,这意味着最多需要等待59分59秒!这显然不可接受。综合以上考虑,Google选择了30秒作为时间片,T的数值为从Unix epoch(1970年1月1日 00:00:00)来经历的30秒的个数
2.不在同一间隔内如何处理?
由于网络延时、用户输入延迟等因素,可能当服务器端接收到一次性密码时,T的数值已经改变,这样就会导致服务器计算的一次性密码值与用户输入的不同,验证失败。解决这个问题个一个方法是,服务器计算当前时间片以及前面的n个时间片内的一次性密码值,只要其中有一个与用户输入的密码相同,则验证通过。当然,n不能太大,否则会降低安全性
如果客户端和服务器的时钟有偏差,会造成与上面类似的问题,也就是客户端生成的密码和服务端生成的密码不一致。但是,如果服务器通过计算前n个时间片的密码并且成功验证之后,服务器就知道了客户端的时钟偏差。因此,下一次验证时,服务器就可以直接将偏差考虑在内进行计算,而不需要进行n次计算
3.如何保证安全?
在扫描 GitHub 给的绑定二维码时,如果被别人拍照、截图、数据劫持,二维码中的秘钥就会泄露,或者Authenticator 应用存储的秘钥被泄露,手机丢失、Authenticator 服务器被攻击等导致秘钥泄露,别人拿到了秘钥之后按照 TOTP 算法生成验证码肯定是有效的,不过前提是对方知道了你的 GitHub 的账号和密码。
所以,选择一款可靠的 Authenticator 应用很关键,建议使用大厂的应用或者开源应用,甚至自己开发一款 TOTP 验证码生成应用。