MoreRSS

site iconGorpeln Chen修改

致力于成为一名架构师的的iOS工程师。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Gorpeln Chen的 RSS 预览

定期自动备份又拍云存储

2024-10-05 08:00:00

前言

数据无价,备份无忧。

将其他平台数据备份到不同平台的好处:

  • 增强数据安全性:通过定期备份到GitHub,即使又拍云服务出现问题,你的数据仍然安全。这为你的数据提供了额外的一层保护,尤其是使用国内免费服务的用户。
  • 便于版本管理:GitHub支持Git版本控制系统,这意味着你可以轻松追踪数据的变化历史,这对于需要保存多个版本的数据集或文档非常有用。
  • 自动化操作:使用GitHub Actions,你可以设置自动化的工作流来定期执行备份脚本。这不仅减少了手动操作的需求,还降低了人为错误的风险。
  • 成本效益:对于小规模项目或个人开发者来说,使用GitHub作为备份存储可能比租用额外的云存储空间更经济。特别是当使用GitHub的免费层级时。
  • 易于访问和分享:GitHub上的数据可以通过简单的链接分享给他人,这对于公开数据集或开源项目尤其有益。

预览

又拍云目录: 20241005111856740

github目录: 20241005111856741

操作示例

1. 新增 Workflow YML 文件

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

2. 新建 Python 同步脚本

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)

3. 授予 Workflow 读写权限

将本地的项目提交到github,然后设置Workflow 读写权限

20241005111856742

4. 新增 Secrets 变量

在github上设置Secrets 变量

20241005111856743

序号 变量名 释义
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)

变量详解:

  1. UPYUN_FTP_HOST:参考又拍云文档,直接使用v0.ftp.upyun.com就行了
  2. UPYUN_FTP_USER:格式为 operator/bucket,在又拍云 - 云存储 - 选择对应的bucket点击配置 - 存储管理 - 操作员授权 - 自己添加操作用户名和密码,假设你的又拍云云储存bucket的名称为upai-img,自定义的用户名为user,密码为123456,则UPYUN_FTP_USER就是 user/upai-img,如果你看不懂我说的,参考 又拍云视频教程
  3. UPYUN_FTP_PASSWORD:就是上面你自己设置的 123456图片示例
  4. UPYUN_FTP_PATH:直接使用 / (根目录)就行了
  5. GH_TOKEN:选择github的 Settings - 点击右侧列表最下面的 Developer Settings - Personal access tokens - tokens (classic) - Generated new token (classic),Note名字可以随意,Expiration时间选择 no Expiration,下面权限全选了,点击Generated token 就生成了想要的token,记得保存,他只显示一次,示例:abc_123456789pUS123454321WmJkE987654321图片示例
  6. REPO_OWNER:就是你的github的用户名,我在github给这个项目创建仓库为https://github.com/gorpeln/upyun_images_sync,示例:gorpeln
  7. REPO_NAME:仓库链接后面的就是仓库名,示例:upyun_images_sync图片示例
  8. BRANCH_NAME:项目分支名,一般为master/main,示例:master

博客常见问题

2024-09-17 08:00:00

博客目标?

简洁、高效

如何联系博主?

可以采用邮件联系(请手动将邮件地址中的*替换掉),博主看到后会及时回复的。

评论加载不出来?

博客已开启CDN,整体访问速度较快。
但博客评论、公告、实验室等功能采用不同域名访问,可能存在无法访问的情况。
如果可能,请科学上网。或者多次刷新加载(5-6次),就可以正常展示。

博客成长记录?

查看博客成长记录

友链相关问题?

查看友链相关问题

博客相关协议?

查看博客相关协议

其他问题?

正在整理中...

防止网站被恶意镜像

2024-08-24 08:00:00

什么是网站镜像?

网站镜像是指在互联网上出现一个与你的网站几乎一模一样的复制品,除了域名不同之外,其它所有内容都完全相同。这包括网站的布局、LOGO、版块结构等。

通常,网站被恶意镜像的情况主要有两种:

  • 完全镜像:这种情况是创建一个与您网站内容完全相同的站点,除了网址不同,其他一切都模仿您的设计和内容。
  • 内容抓取:这种情况常见于博彩网站,它们会恶意抓取您的网页内容,正常浏览时隐藏掉原有内容,显示的却是他们自己的广告。

20240824212126440

在上述例子中,明显属于第一种情况:对方镜像了我的网站布局和结构。这种行为属于灰色 SEO 和黑帽 SEO 手法,目的是为了借助我博客的权重和流量来提升他们自己网站的排名。 20240824212126441

网站被镜像危害

从搜索引擎来讲,会对搜索引擎抓取不利,影响原本网站的正常抓取和识别,可能会导致原网站权重丢失,也有可能带来一定的误伤,搜索引擎会对网站进行识别,如果发现是镜像站,会导致网站被搜索引擎屏蔽,将失去搜索引擎带来的流量(这对个人博客来说是致命的)。

对用户来讲,可能会被镜像网站欺骗,恶意广告插播或欺诈内容造成用户损失。

网站被镜像了怎么办

添加防镜像跳转代码

<!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)));}()));

效果示意

20240824212126442

其他方法

1、向 google、域名注册商 、域名解析商举报

  • cloudflare 举报:https://abuse.cloudflare.com/dmca
  • google 举报:https://support.google.com/legal/answer/3110420?visit_id=638624243885343005-291301472&rd=1
  • 阿里云举报:https://report.aliyun.com/form/phishingForm

2、设置 IP 黑名单

  • 进入自己的服务器设置或者是虚拟主机管理,找到防火墙有的叫黑名单,把对方的 IP 地址添加进行,屏蔽访问和请求自己网站服务器

3、向谷歌申请移除镜像站的搜索结果

  • 因为它会原样照搬反代网页,那其实你可以通过HTML 标记验证方式在 Google Search Console 里把他的反代域名也认证到你自己名下,然后在Google Search Console 向谷歌申请移除搜索结果,嘿嘿😋

Follow - 订阅一切

2024-07-06 08:00:00

Follow是什么?

Follow 是一款面向未来的信息浏览器,致力于将各种信息源整合在一个平台上,让用户便捷地获取和管理资讯。无论是传统的 RSS 订阅、社交媒体账号、博客、播客,还是实时通知,Follow 都能帮您一站式浏览。界面简洁,操作迅速,为用户提供现代化的阅读体验。

Follow的slogan是:Next generation information browser

Follow的主要特色:

  • 信息一体化管理:Follow 支持广泛的信息源,除了传统的 RSS,还包括 Twitter、Instagram、YouTube 等社交平台,用户可以在一个界面中浏览全部订阅内容,方便直观。

  • 智能化 AI 功能:内置的 AI 不仅能实现内容翻译和摘要,还能每日生成两次个性化报告,提炼重要信息,帮助用户轻松跟踪最关心的动态。此外,Follow 还能根据您的订阅喜好建立个性化知识库。

  • 区块链激励机制:Follow 通过区块链技术为活跃用户和内容创作者提供动力代币奖励,鼓励用户积极参与和分享内容,提升整体使用体验。

  • 社交功能:Follow 也具备社交平台属性,用户可以关注他人,分享订阅列表,发现新内容。支持与好友同步订阅列表,便捷地分享和发现优质资源。

20240706111415430

Follow的访问隐忧

  • 由于大平台都可以进行订阅,这种行为在某种程度上会触动大平台的利益。
  • follow的服务器并不在国内,且爬虫本身就自带法律风险。这些都有可能影响到国内对follow应用的访问。
  • 即使现在可以使用,也并不代表未来能一直访问(直连)。

Follow认证

Follow的另一个亮点是支持打赏功能,你可以给你喜欢的文章进行打赏,当然前提是这个文章所有者进行了Follow认证,打赏的金额才能进入到他的账户中去。Follow使用的打赏货币名为power,是一种区块链货币,按照官方的说法会根据在Follow上的活跃度和贡献值来获取power。

1.获取认证码

在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.

2.验证认证码

在你自己博客中发布一篇博文,内容为刚生成的认证码,然后在Follow中再次点击申请认证(Claim),即可完成认证。认证完成后认证码的相关博文信息就可以删除了。

Follow订阅

目前gorpeln’s Blog已经过认证,在搜索框中直接输入『 gorpeln 』可直接订阅我的更新动态。或者直接填入gorpeln的RSS地址也是可以的 https://gorpeln.top/feed.xml

20240706111415431

其他top订阅可以访问github开源项目top-rss-list

双因素认证(2FA)实现原理

2024-06-29 08:00:00

我们往往会在不同的网站上使用相同的密码,这样一旦一个网站账户的密码泄露,就会危及到其他使用相同密码的账户的安全,这也是最近的密码泄露事件造成如此大影响的原因。为了解决这个问题,一些网站在登录时要求除了输入账户密码之外,还需要输入另一个一次性密码,这就是常说的多步验证(多因子认证)。

多因子认证

多因子认证(Multi-factor Authentication),简称MFA。在现代网络安全领域,多因子认证已成为保护用户账户安全的重要手段,它要求用户提供两个或更多独立的认证因子,例如:账号密码+短信验证码、账号密码+邮箱验证码、账号密码+TOTP验证码等,当然对于Gtihub要求的2FA,指的是Two-Factor Authentication双因子认证,与MFA的主要区别是认证因子的数量,从更广义的层面来看,2FA可以看作是MFA的一种特殊情况,即MFA的一个子集

MFA的主要目的是增强安全性。相比于单一的用户名和密码认证,MFA增加了额外的验证方式,从而显著降低了账户被入侵的风险。即便攻击者获得了账号的用户名和密码,仍需要其他认证因子才能访问账户,极大地提高了账户的安全性

常见的多因子认证方式有:

  1. 短信验证码:这种验证方式非常广泛,在用户在登录时,系统会发送一个一次性验证码到用户的手机上,用户则需要在登录页面输入这个验证码来完成身份验证,这种认证方式的优点是简单方便,用户无需额外的设备或应用就能完成二次验证,缺点则主要是会产生短信费用,同时也可能会受到SIM卡交换攻击或短信拦截

  2. 邮件验证码:系统发送一次性验证码到用户的电子邮箱,用户需要在登录页面输入该验证码完成验证,邮箱验证码使用也较为广泛,这种认证方式优点是易于实现和使用,用户无需额外设备,只要有邮箱即可,缺点则主要是邮件响应速度较慢,用户体验不佳,同时安全性较低,邮箱可能被黑客入侵

  3. 生物识别认证:使用指纹、面部识别或声纹等生物特征进行身份验证,常用于高安全性需求场景,如解锁智能手机或访问敏感系统。这种认证方式优点是安全性极高,唯一性强、用户体验也好,快速便捷,缺点主要是设备要求高,需要设备支持指纹、面部解锁等,其次一些场景无法支持,例如WEB端,同时也有隐私问题需考虑

  4. 硬件令牌:这种常见于财务系统,使用独立的硬件设备生成一次性密码或进行物理验证,用户将硬件令牌插入计算机或使用NFC读取器完成验证。优点是极高的安全性,同时也能防止网络攻击,因为它不依赖移动设备或互联网连接,缺点也比较明显,依赖硬件,成本比较高,使用不方便

  5. 基于时间的一次性密码(TOTP):基于时间生成的一次性密码,有标准的算法,通过标准算法来生成验证码,有许多免费的应用或小程序都能生成,用户需要输入应用生成的验证码来完成身份验证。这种认证方式的主要优点是安全性高,不依赖网络传输,多数MFA应用免费提供,缺点则主要是用户需要额外安装并配置应用

  6. 基于事件的一次性密码(HOTP):基于事件计数生成的一次性密码,类似于TOTP,但使用事件计数而非时间。适用于硬件令牌。这种认证方式的优点是不依赖时间同步,适用于硬件设备,缺点则主要是使用体验不如TOTP方便

  7. 推送通知:通过认证应用发送推送通知到用户的设备,用户只需点击“批准”或“拒绝”即可完成身份验证,例如之前遇到过的Google账号认证,会发送消息到你登录Google账号的可信手机上,点击确定就能登录。这种认证方式的优点是:用户体验好,方便快捷、更加安全,防止中间人攻击,缺点则是需要互联网连接、依赖移动设备,也不是所有设备都支持

  8. 安全问题:用户设置一系列安全问题,登录时需回答这些问题进行验证。这种认证方式的优点是实施简单,无需额外设备,缺点则主要是安全性较低,容易被猜测或社交工程攻击,长时间不使用还容易遗忘,例如我小时候申请的QQ号密保问题是:你的梦想是什么?现在已经完全想不起来答案了

其中,基于时间的一次性密码TOTP是一种非常流行且标准化的多因子认证方式,尤其是在Web系统中,例如阿里云、腾讯云、Github、Google等诸多知名网站都支持TOTP多因子认证,另外一些应用较广的运维相关开源软件也都加入了对TOTP二次认证的支持,例如CODO、Jumpserver等,甚至出现了多因子认证约等于TOTP的现象

TOTP

2fa

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的工作原理可以概括如下:

  1. HMAC算法:TOTP是基于HMAC(Hash-Based Message Authentication Code)算法构建的,它结合了一个共享的秘密密钥和当前时间来生成一次性密码。
  2. 共享密钥:在设置Google Authenticator时,用户的设备和验证服务器之间会共享一个密钥。这个密钥是安全存储的,并且对于每个账户是唯一的。
  3. 时间作为参数:TOTP算法使用当前的时间作为变化因素。时间通常被分割成固定的时间片段(例如,每30秒一个片段)。
  4. 生成一次性密码
    • 首先,当前时间被转换为一个计数器值,这通常是自Unix纪元(1970年1月1日)以来的30秒间隔数。
    • 然后,使用HMAC算法结合这个计数器值和共享密钥来生成一个哈希值。
    • 最后,从这个哈希值中提取一个较短的数字序列,通常是6到8位数,作为一次性密码。
  5. 验证一次性密码:当用户输入一次性密码时,验证服务器也会独立生成一个密码,使用相同的共享密钥和当前时间。如果两者匹配,验证成功。

TOTP的优点:

  • 密码动态生成:由于密码是基于当前时间生成的,它每30秒就会变化一次,并且只能使用一次,这种机制有效防止了密码重用和暴力破解攻击,提高了账户的安全性
  • 无需网络:生成密码不依赖于网络连接,因为它基于时间和共享密钥,这增加了安全性和可用性
  • 成本低:相比硬件令牌等其他MFA方式,TOTP的成本更低,用户只需在手机上安装一个免费的应用程序即可实现多因子认证,而无需购买额外的硬件设备。对于企业而言,这降低了部署MFA的成本和复杂性
  • 标准化:TOTP遵循RFC 6238标准,确保了其跨平台的兼容性和一致性

举一个不恰当但简单易懂的例子:首先使用手机扫描平台的二维码从服务器获取秘钥(假如为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的算法是公开的,所以生成TOTP验证码也较为简单,国内各大云厂商的APP或是一些小程序也都有提供虚拟MFA的功能,例如腾讯云助手小程序等等,知名的MFA应用主要有Google验证器(Google Authenticator)和微软验证器(Microsoft Authenticator)

开启2FA

以Github为例开启2FA。

  • Settings -> Password & authentication -> Two-factor authentication -> Authenticator app
  • 生成绑定二维码
  • TOTP 应用/插件扫码绑定
  • 登录账号后输入 TOTP 验证码

问题解答

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 验证码生成应用。