本文介绍 Token 认证和 HMAC 认证两种方式。
一、Token 接口认证方式
原理:
客户端使用账号密码等信息登录,服务器验证通过后生成一个 Token 发送给客户端。客户端在后续的请求中携带这个 Token,服务器通过验证 Token 来确认用户的身份和权限。
应用场景:移动应用、Web 应用(特别是 SPA)
优点:
无状态性,即服务器不需要存储用户的会话信息。
易于实现跨域认证。
缺点:
Token 可能被窃取,应使用 HTTPS、不暴露在 URL 中、使用 HttpOnly 的 Cookie、对 Session ID 进行验证、设置合理过期时间、对 Token 进行加密等措施加强防范。
二、HMAC 接口认证方式
原理:
客户端将消息M与密钥K连接起来,通过哈希函数计算得到 HMAC 值,发送给服务器。服务器收到请求后,使用相同的密钥和请求参数重新计算 HMAC 值,如果与客户端发送的签名一致,即是合法请求。
优点:
安全性较高,攻击者很难伪造 HMAC 值,截获并篡改数据也无法通过服务端验证。
计算效率较高,哈希函数(如 MD5、SHA-1、SHA-256 等)计算效率比较高。
缺点:
密钥的更新和管理比较麻烦。
扩展:
在消息体中添加时间戳以防止重放攻击。
加密隐私数据:可以使用对称加密算法(如 AES)或非对称加密算法(如 RSA、ECC 等)对部分隐私数据进行加密。非对称算法虽然更安全,但速度较慢,如需加密大量数据,可以考虑使用对称加密算法进行加密,然后使用非对称加密算法对对称密钥进行加密。

http {
...
lua_shared_dict cpu_cache 1m; # 定义共享字典用于缓存 CPU 使用率
access_by_lua_block {
local cpu_cache = ngx.shared.cpu_cache
local cache_time = cpu_cache:get("cache_time") or 0
local current_time = ngx.now()
if current_time - cache_time > 5 then -- 每 5 秒更新一次
local cpu_info = io.popen("top -bn1 | grep 'Cpu(s)'"):read("*all")
local cpu_usage = cpu_info:match("(%d+%.%d+) id") -- 获取空闲 CPU 百分比
local cpu_used = 100 - tonumber(cpu_usage) -- 计算使用率
cpu_cache:set("cpu_usage", cpu_used)
cpu_cache:set("cache_time", current_time)
end
local cpu_usage = cpu_cache:get("cpu_usage")
ngx.log(ngx.INFO, "CPU 使用率: " .. cpu_usage .. "%")
-- 将 cpu_usage 发送到远程鉴权接口,可根据服务器压力来决定是否拒绝一些不重要的请求
}
}
代码解析:
共享字典:使用 lua_shared_dict 定义一个共享字典 cpu_cache,用于存储 CPU 使用率和缓存时间。
获取 CPU 使用率:在 access_by_lua_block 中,检查缓存时间,如果超过 5 秒,则重新获取 CPU 使用率,并更新共享字典。
记录日志:使用 ngx.log 将 CPU 使用率记录到 Nginx 日志中。
注意事项:
确保 Nginx 配置中已经加载了 Lua 模块(如 ngx_http_lua_module)。
根据实际需求调整缓存时间,以平衡性能和数据的实时性。
尝试过使用 ifconfig 或 ip 命令获取网卡流量,在宝塔面板中失败了,怀疑是权限问题,有空再研究。临时方案是鉴权接口定时调用宝塔面板 API 或阿里云控制台 API 来获取 ECS 的 CPU 和带宽使用率。

鉴权方式有许多,譬如可以用 lua 的 access_by_lua 来实现,这里用 nginx 自带的 auth_request,虽然功能简单,但效率更高。
第一步,确保 nginx 已编译安装 auth_request 模块,见此文。
第二步,打开需要鉴权的网站的 nginx 配置文件,添加以下代码块:
#鉴权-START
location / {
auth_request /auth;
error_page 403 = @error403;
#PHP-INFO-START PHP引用配置,可以注释或修改
include enable-php-**.conf;
#PHP-INFO-END
}
location = /auth {
internal;
proxy_pass https://鉴权地址;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_intercept_errors on;
#proxy_read_timeout 1s; # 超时会报 500
}
location @error403 {
#default_type text/plain;
#return 403 'forbidden';
return 302 https://一个显示403友好信息的页面.html;
}
#鉴权-END
一般放在所有的 location 之前。
这里自定义请求头 X-Forwarded-Host
与 X-Forwarded-Uri
用来传递 host 与 uri。API 应从 Header 中相应取值。
宝塔面板中是通过 include enable-php-**.conf;
的方式调用 PHP ,那么可以将此行移入上面的 location /
代码块中,因为此代码块能匹配所有的请求路径。
最后,若鉴权接口在私网中,将鉴权接口域名和私网 IP 添加到 hosts 文件中。

本文基于 manifest v3
插件使用 Web 技术开发
浏览器提供额外的插件 API
插件与网页分离,运行在一个独立的环境中
插件 API 功能
管理 Tabs、窗口、历史记录、书签、Cookies、下载、浏览数据、通知、网站权限
管理浏览器的外观和感觉 - 背景、主题、右键菜单、新标签页、启动页面
定制 DevTools
向网页注入脚本,与网页通信,与系统中的 Native 应用程序通信
修改/监听发送的 HTTP 请求/接收的回复
插件构成
结构 | 功能 | 调用 API | 操作 Dom | 进程 |
manifest | 配置文件 | - | ||
popup | 弹出对话框 | 所有 | Extension process | |
option page | 用户使用的设置页面 | 所有 | ||
background script | 后台脚本 | 所有 | 禁止? | Extension process |
content script | 内容脚本(注入到网页中) | 有限 | 允许 | Renderer process |
* 即便 content script 是注入到网页中的,而且是运行在 Renderer 进程(与主网页相同的进程),但是它们仍运行在不同的世界(world)。主网页运行在 main world,插件的 content script 运行在 isolated world。比如说 main world 中有一个变量,它在 isolated world 中是访问不到的,但是如果修改了 dom,对其它世界是有影响的。
将 Chrome 插件迁移到 Edge
移除 Chrome 独有的 API(如调用 Google 账户)
移动 update_URL 字段(如果是从 Chrome 商店直接下载的包会有这个字段)
改名 Chrome 相关的文字(插件名称、描述文字)
参考文献

SignalR 是一个开源的实时通信库,用于构建实时 Web 应用程序。它提供了一个简单的 API,可以在客户端和服务器之间建立持久连接,以便实时地推送数据。
与传统的 WebSocket 相比,SignalR 提供了更高级的功能和更简单的开发体验。下面是一些主要区别:
支持多种传输方式:SignalR 可以使用多种传输方式,包括 WebSocket、Server-Sent Events(SSE)、长轮询和 Forever Frame。这使得 SignalR 在不同的环境中都能提供实时通信的能力,即使某些浏览器不支持 WebSocket,也可以使用其他传输方式。
自动处理连接管理:SignalR 管理连接的生命周期,包括连接的建立、断开和重新连接。它会自动处理连接的失败和重新连接的逻辑,简化了开发人员的工作。
服务器端推送:SignalR 允许服务器端主动推送消息给客户端,而不需要客户端发起请求。这使得实时通信变得更加高效和实时,适用于聊天应用、实时监控等场景。
跨平台支持:SignalR 可以在多个平台上使用,包括 .NET、Java、JavaScript 等。这使得开发人员可以使用自己熟悉的语言和框架来构建实时应用程序。
微软官方提供了针对 ASP.NET Core Web 应用(Razor 页面)的详细教程,这里给出 MVC 版本入门教程。
最终将创建一个正常运行的聊天应用:
创建 Web 应用项目
添加 SignalR 客户端库
在“解决方案资源管理器”>中,右键单击项目,然后选择“添加”“客户端库”。
在“添加客户端库”对话框中:
“提供程序”选择“unpkg”
“库”,请输入 @microsoft/signalr@latest。
选择“选择特定文件”,展开“dist/browser”文件夹,然后选择 signalr.js 和 signalr.min.js。
点击“安装” 。
创建 SignalR Hubs 类
using Microsoft.AspNetCore.SignalR;
/// <summary>
/// Hub 类管理连接、组和消息
/// </summary>
public class ChatHub : Hub
{
/// <summary>
/// 可通过已连接客户端调用 SendMessage,以向所有客户端发送消息
/// </summary>
public async Task SendMessage(string user, string message)
{
// Clients.All 向所有的客户端发送消息
// ReceiveMessage 是客户端监听的方法
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
其父类 Hub 可管理连接、组和消息。这里演示的是向所有客户端发送消息。
配置 SignalR
打开 Program.cs,添加注入:
builder.Services.AddSignalR();
添加路由:
app.MapHub<ChatHub>("/chatHub");
添加 SignalR 客户端代码
视图页面:
<div class="container">
<div class="row p-1">
<div class="col-1">用户</div>
<div class="col-5"><input type="text" id="userInput" /></div>
</div>
<div class="row p-1">
<div class="col-1">消息</div>
<div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input type="button" id="sendButton" value="发送消息" />
</div>
</div>
<div class="row p-1">
<div class="col-6">
<hr />
</div>
</div>
<div class="row p-1">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/chat.js"></script>
chat.js 文件:
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
// 在连接建立之前禁用发送按钮
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
// 修改此处时应注意脚本注入问题
li.textContent = `${user} says ${message}`;
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
完成。在线示例:https://xoyozo.net/Demo/SignalRDemo

如果手机开启了自动横屏,那么网页就会跟着旋转,导致网页宽度变大,高度变小,页面布局不友好,我们可以通过判断屏幕方向 api,用 css 设置旋转。
HTML:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>保持竖屏示例</title>
</head>
<body>
<div id="app">
<div class="page">
<div>把手机横过来试试</div>
<div>页面永远保持竖屏方向</div>
</div>
</div>
</body>
</html>
CSS:
body { margin: 0; }
.page { text-align: center; padding-top: 100px; }
@media screen and (orientation: landscape) {
body { width: 100vh; height: 100vw; overflow: hidden; }
.page.landscape-primary { transform: rotate(-90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 100%; left: 0; }
.page.landscape-secondary { transform: rotate(90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 0; left: 100%; }
}
JS:
function fn_set_orientation() {
var orientation = window.screen.orientation || window.screen.mozOrientation || window.screen.msOrientation;
const div = document.querySelector('.page');
console.log(orientation.type)
switch (orientation.type) {
case 'landscape-primary': {
div.classList.remove('landscape-secondary');
div.classList.add('landscape-primary');
break;
}
case 'landscape-secondary': {
div.classList.remove('landscape-primary');
div.classList.add('landscape-secondary');
break;
}
default: {
div.classList.remove('landscape-primary');
div.classList.remove('landscape-secondary');
break;
}
}
}
window.addEventListener("orientationchange", function () {
fn_set_orientation();
});
fn_set_orientation();
示例:
https://xoyozo.net/Demo/Orientation

可能的原因是 IIS 安装了“WebDAV 发布”,在 IIS - 模块 中可见 WebDAVModule
解决方法:在网站的 web.config 中设置:
<system.webServer>
<modules>
<remove name="WebDAVModule" />
</modules>
</system.webServer>
或在 IIS 中,在该网站的模块中删除 WebDAVModule。
如果网站需要用到 WebDAV 模块,那么考虑使用 Fetch API 或 XMLHttpRequest 来发送请求。

解析域名(非网站域名)、挂载磁盘(若有另购)、修改实例名称、主机名
设置阿里云(重要)
远程连接进入 ECS(若解析未生效可以先用 IP)(若新服默认使用 3389 端口,可先在安全组临时放行 3389 端口)
开启 Windows 防火墙(使用推荐设置)
Windows 更新、并在高级选项中开启(更新 Windows 时接收其它 Microsoft 产品的更新)
安装 IIS:服务器管理器-添加角色和功能-勾选“Web 服务器(IIS)”包括管理工具
建议勾选:
默认已勾选项
按需安装 IP 和域限制
跟踪(即“失败请求跟踪”)
请求监视器、日志记录工具、
按需安装 ASP
按需安装 ASP.NET 4.8(会同时勾选 .NET Extensibility 4.8、ISAPI 扩展、ISAPI 筛选器)
按需安装 WebSocket 协议
应用程序初始化(建议安装)
管理服务(用于 Web 部署)
细节:设置任务栏;设置桌面图标;个性化-颜色-勾选“标题栏和窗口边框”;设置输入法;
下载 URL 重写(文件名:rewrite_amd64_zh-CN.msi)
下载 MySQL Connector/NET(文件名:mysql-connector-net-8.0.19.msi)
下载 ASP.NET Core 运行时 Hosting Bundle(文件名:dotnet-hosting-*.*.*-win.exe)
下载 .NET 桌面运行时 Windows x64(文件名:windowsdesktop-runtime-*.*.*-win-x64.exe)
下载 Web Deploy(文件名:WebDeploy_amd64_zh-CN.msi)
服务:设置“ASP.NET State Service”自动启动
IIS 日志:路径(如 D:\wwwlogs),每小时(统一设置一个全局的就行了,不需要设置每个网站),按需勾选“使用本地时间进行文件命名和滚动更新”
IIS 导入证书:个人、允许导出证书。参
设置权限:设置网站所在分区(如 D 盘),安全,添加 IIS_IUSRS,全部拒绝(防止跨站)
添加用户:为每个网站创建用户(既能防止跨站,又能跟踪进程),密码不能改、不过期,仅隶属于 IIS_IUSRS,并添加到每个网站的根目录,若用户创建失败看这里。
创建网站:设置访问物理路径的用户;设置应用程序池的“标识”用户;编辑绑定:勾选需要服务器名称指示;检查域名是否绑全;设置写入目录;
重复上面两步
检查所有网站用户是否仅隶属于 IIS_IUSRS(在“组”页面双击 Users 和 IIS_IUSRS 查看成员)
在应用程序池列表页面检查 CLR 版本、管托管道模式和标识;在网站列表页面检查绑定和路径
设置“IP 地址和域限制”
废弃旧服时再次检查:IIS 中各功能设置、hosts、安装的应用程序、启动项、服务、防火墙等
解析各网站域名
其它:资源管理器-查看-选项-查看-去掉“始终显示图标,从不显示缩略图”前的勾
再次检查阿里云设置
在备份工具中添加该服务器的所有备份项
其它:到期日期提醒、
>> 关于域名解析
因各地域名解析生效时间不可控,一般国内域名 1 天内,国际域名 2 天内。
若网站数据库在 RDS、上传文件在 OSS,则解析 48 小时后直接停止原网站即可;(比较理想的)
文件上传到 ECS 的可使用 FTP 等工具定时同步文件,或直接停止原网站。(网友会遇到新文章中图片无法显示等问题)
还有一种方法是新网站提前解析一个备用域名,确保完全生效后再修改正式域名的解析,原网站无条件跳转到备用域名,如果数据库中有保存完整网址路径的,关闭原网站并解绑备用域名之后,进行批量替换。(缺点是可能会影响在搜索引擎的网站权重)
部分有定时器的网站要注意,如果两个网站的定时器都正常开启会导致意外的,需要停止其中一个网站的定时器。
当然每种方法都有优缺点,选择可以接受且方便的一种即可。
更多文章:

语言集成查询(LINQ)为 C# 和 VB 提供语言级查询功能和高阶函数 API,让你能够编写具有很高表达力度的声明性代码。
LINQ 有两种写法:查询语法和方法语法,查询语法又称查询表达式语法。
查询语法:
from 变量名 in 集合 where 条件 select 结果变量
方法语法:
集合.Where(变量名 => 条件)
LINQ 的标准查询运算符及语法示例
类型 | 操作符 | 功能 | 方法语法 | 查询语法 |
投影操作符 | Select | 用于从集合中选择指定的属性或转换元素 |
|
|
SelectMany | 用于在嵌套集合中选择并平铺元素 |
|
| |
限制操作符 | Where | 根据指定的条件筛选集合中的元素 |
|
|
排序操作符 | OrderBy、OrderByDescending、ThenBy、ThenByDescending | 用于对集合中的元素进行排序 |
|
|
Reverse | 将集合中的元素顺序反转 |
| ||
联接操作符 | Join GroupJoin | 用于在两个集合之间执行内连接(Join)操作,或者对一个集合进行分组连接(GroupJoin)操作 | 内联接
左连接
| 内联接
左连接
|
分组操作符 | GroupBy | 根据指定的键对集合中的元素进行分组 | ||
串联操作符 | Concat | 将两个集合连接成一个新集合 | ||
聚合操作符 | Aggregate、Average、Count、LongCount、Max、Min、Sum | Aggregate 可以用于在集合上执行自定义的累积函数,其他方法用于计算集合中的元素的平均值、总数、最大值、最小值和总和 | ||
集合操作符 | Distinct、Union、Intersect、Except | 用于执行集合间的不同操作,Distinct 移除重复元素,Union 计算两个集合的并集,Intersect 计算两个集合的交集,Except 计算一个集合相对于另一个集合的差集 | ||
生成操作符 | Empty、Range、Repeat | Empty 创建一个空集合,Range 创建一个包含一系列连续数字的集合,Repeat 创建一个重复多次相同元素的集合 | ||
转换操作符 | AsEnumerable、Cast、OfType、ToArray、ToDictionary、ToList、ToLookup | 这些方法用于将集合转换为不同类型的集合或字典 | ||
元素操作符 | DefaultIfEmpty、ElementAt、ElementAtOrDefault、First、Last、FirstOrDefault、LastOrDefault、Single、SingleOrDefault | 这些方法用于获取集合中的元素,处理可能的空集合或超出索引的情况 | ||
相等操作符 | SequenceEqual | 用于比较两个集合是否包含相同的元素,顺序也需要相同 | ||
量词操作符 | All、Any、Contains | 用于检查集合中的元素是否满足特定条件,All 检查是否所有元素都满足条件,Any 检查是否有任何元素满足条件,Contains 检查集合是否包含特定元素 | ||
分割操作符 | Skip、SkipWhile、Take、TakeWhile | 用于从集合中跳过一些元素或只取一部分元素,可以结合特定条件进行操作 |
了解立即执行与延迟执行可以大大改善性能。

