在 access_by_lua_block 代码块中实现远程鉴权:
#鉴权-START resolver 223.5.5.5; # 使用公共 DNS access_by_lua_block { local http = require("resty.http") local httpc = http.new() httpc:set_timeout(500) -- 连接超时 local res, err = httpc:request_uri("https://鉴权地址/", { method = "GET", headers = { ["X-Real-IP"] = ngx.var.remote_addr, ["User-Agent"] = ngx.var.http_user_agent, ["X-Forwarded-Host"] = ngx.var.host, ["X-Forwarded-Uri"] = ngx.var.request_uri, }, ssl_verify = false, -- 禁用 SSL 验证 timeout = 500, -- 读取超时 }) if not res then ngx.log(ngx.ERR, "Failed to request: " .. err) end if res and res.status == 403 then ngx.exit(ngx.HTTP_FORBIDDEN) -- return ngx.redirect("https://一个显示403友好信息的页面.html") end } #鉴权-END
注意更改接口地址和友情显示 403 页面地址。
本示例仅捕获 403 状态码,500、408 等其它异常情况视为允许访问,请根据业务需求自行添加状态码的判断。
若超时也会进入
if not res then
代码块。建议将此代码部署在 nginx 主配置文件的 http 代码块中(宝塔面板中的路径:/www/server/nginx/nginx/conf/nginx.conf),如果你只想为单个网站鉴权,也可以放在网站配置文件的 server 块中。
若鉴权接口在私网中,建议将鉴权接口域名和私网 IP 添加到 hosts 文件中。
附
直接输出字符串
ngx.header.content_type = "text/plain"; ngx.say("hello world!")
输出到日志
ngx.log(ngx.ERR, "Response status: " .. res.status)
日志在网站的 站名.error.log 中查看。
宝塔面板查看方式:日志 - 网站日志 - 异常
若想获取服务器 CPU 使用率等信息并传递给远程鉴权接口,请参考此文。
常见问题
no resolver defined to resolve
原因:没有定义 DNS 解析器
解决方法:在 http 块或 server 块中添加
resolver 8.8.8.8 valid=30s;
,当然使用接入商自己的公共 DNS 可能更合适。unable to get local issuer certificate
原因:没有配置 SSL 证书信息
解决方法:添加 request_uri 参数:
ssl_verify = true, -- 启用 SSL 验证 ssl_trusted_certificate = "证书路径", -- 指定 CA 证书路径
或
ssl_verify = false, -- 禁用 SSL 验证
若您不想用 lua,可以用 nginx 原生自带的 auth_request 模块来实现。
因 MySQL 8 默认使用 utf8mb4 字符集,如果是从 MySQL 5-7 等低版本迁移的,那么仍然是 utf8(相当于 utf8mb3)。
这在保存字符串时有可能会报错:
An error occurred while saving the entity changes. See the inner exception for details.
Incorrect string value: '\xF0\xA1\x90\x93\xE6\x9D...' for column 'Html' at row 1
即使连接字符串上加上 CharSet=utf8 或 CharSet=utf8mb3 或 CharSet=utf8mb4 也没用。
那么只能把数据库的字符集改为 utf8mb4。
更改字符集和排序方式前必须先备份数据库!
更改现有数据库的字符集和排序规则
ALTER DATABASE mydatabase CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
更改现有表的字符集和排序规则
ALTER TABLE mytable CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
更改现有列的字符集和排序规则
当列单独设置了字符集时执行,未设置的不需要执行,会继承表的字符集
ALTER TABLE mytable MODIFY mycolumn VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
数据库中可能存在许多表,数据表中可能存在许多列,使用下面的命令可以生成批量执行的命令:
更改所有表的默认字符集和排序规则
USE database_name; -- 替换为你的数据库名 SELECT CONCAT('ALTER TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'database_name'; -- 再次替换为你的数据库名
更改所有列的字符集和排序规则
USE database_name; -- 使用你的数据库名 SELECT CONCAT('ALTER TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '` MODIFY `', COLUMN_NAME, '` ', COLUMN_TYPE, ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'database_name'; -- 使用你的数据库名
另外给出几条查询语句:
查看特定数据库的字符集和排序规则
SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'your_database_name';
查看特定表的字符集和排序规则
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_database_name';
查看特定列的字符集和排序规则
SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'your_database_name' AND TABLE_NAME = 'your_table_name';
在 Vue 中,当你将一个布尔对象双向绑定到 radio 输入上时,确保 radio 的 value 属性仍然保持布尔类型是很重要的。如果 value 属性是字符串,那么 Vue 会将其解释为字符串而不是布尔值。以下是一个示例,演示如何正确地将布尔对象双向绑定到 radio 按钮,并确保 value 属性保持布尔类型:
<template>
<div>
<input type="radio" v-model="myBoolean" :value="true" /> True
<input type="radio" v-model="myBoolean" :value="false" /> False
<p>myBoolean: {{ myBoolean }}</p>
</div>
</template>
<script>
export default {
data() {
return {
myBoolean: false, // 布尔对象
};
},
};
</script>
在这个示例中,我们使用了 v-model 指令将 myBoolean 属性与两个 radio 按钮进行了双向绑定。通过将 value 属性设置为 true 和 false,我们确保了 myBoolean 属性保持布尔类型。
这样,当用户选择其中一个 radio 按钮时,myBoolean 属性将保持布尔值而不是字符串。确保 value 属性的类型与要绑定的数据类型匹配,以确保绑定的数据类型保持一致。
同样的问题出现在 select 上也是一样:
<select v-model="cat.colorId" required>
<option :value="null">不选择</option>
<option v-for="color in colors" :value="color.id">{{color.name}}</option>
</select>
不知道从哪个版本的 Chrome 或 Edge 开始,我们无法通过 ctrl+v 快捷键将时间格式的字符串粘贴到 type 为 date 的 input 框中,我们想办法用 JS 来实现。
方式一、监听 paste 事件:
const input = document.querySelector('input[type="date"]');
input.addEventListener('paste', (event) => {
input.value = event.clipboardData.getData('text');
});
这段代码实现了从页面获取这个 input 元素,监听它的 paste 事件,然后将粘贴板的文本内容赋值给 input。
经测试,当焦点在“年”的位置时可以粘贴成功,但焦点在“月”或“日”上不会触发 paste 事件。
方式二、监听 keydown 事件:
const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
event.preventDefault();
var clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
input.value = clipboardData.getData('text');
}
});
测试发现报错误:
Uncaught TypeError: Cannot read properties of undefined (reading 'getData')
Uncaught TypeError: Cannot read properties of undefined (reading 'clipboardData')
看来 event 中没有 clipboardData 对象,改为从 window.navigator 获取:
const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
event.preventDefault();
window.navigator.clipboard.readText().then(text => {
input.value = text;
});
}
});
缺点是需要用户授权:
仅第一次需要授权,如果用户拒绝,那么以后就默认拒绝了。
以上两种方式各有优缺点,选择一种适合你的方案就行。接下来继续完善。
兼容更多时间格式,并调整时区
<input type="date" /> 默认的日期格式是 yyyy-MM-dd,如果要兼容 yyyy-M-d 等格式,那么:
const parsedDate = new Date(text);
if (!isNaN(parsedDate.getTime())) {
input.value = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
}
以 text 为“2023-4-20”举例,先转为 Date,如果成功,再转为英国时间格式“20-04-2023”,以“/”分隔,逆序,再以“-”连接,就变成了“2023-04-20”。
当然如果希望支持中文的年月日,可以先用正则表达式替换一下:
text = text.replace(/\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*/, "$1-$2-$3");
处理页面上的所有 <input type="date" />
const inputs = document.querySelectorAll('input[type="date"]');
inputs.forEach((input) => {
input.addEventListener(...);
});
封装为独立域
避免全局变量污染,使用 IIFE 函数表达式:
(function() {
// 将代码放在这里
})();
或者封装为函数,在 jQuery 的 ready 中,或 Vue 的 mounted 中调用。
在 Vue 中使用
如果将粘贴板的值直接赋值到 input.value,在 Vue 中是不能同步更新 v-model 绑定的变量的,所以需要直接赋值给变量:
<div id="app">
<input type="date" v-model="a" data-model="a" v-on:paste="fn_pasteToDateInput" />
{{a}}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const app = Vue.createApp({
data: function () {
return {
a: null,
}
},
methods: {
fn_pasteToDateInput: function (event) {
const text = event.clipboardData.getData('text');
const parsedDate = new Date(text);
if (!isNaN(parsedDate.getTime())) {
const att = event.target.getAttribute('data-model');
this[att] = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
}
},
}
});
const vm = app.mount('#app');
</script>
示例中 <input /> 添加了 data- 属性,值同 v-model,并使用 getAttribute() 获取,利用 this 对象的属性名赋值。
如果你的 a 中还有嵌套对象 b,那么 data- 属性填写 a.b,方法中以“.”分割逐级查找对象并赋值
let atts = att.split('.');
let target = this;
for (let i = 0; i < atts.length - 1; i++) {
target = target[atts[i]];
}
this.$set(target, atts[atts.length - 1], text);
Pomelo.EntityFrameworkCore.MySql 升级到 7.0 后出现:
System.InvalidOperationException:“The 'sbyte' property could not be mapped to the database type 'tinyint(1)' because the database provider does not support mapping 'sbyte' properties to 'tinyint(1)' columns. Consider mapping to a different database type or converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.”
对比 6.0 生成的 DbContext.cs 发现缺少了 UseLoggerFactory,按旧版修改即可。
找到 OnConfiguring 方法,上方插入:
public static readonly LoggerFactory MyLoggerFactory = new LoggerFactory(new[] {
new DebugLoggerProvider()
});
在 OnConfiguring 方法中将:
optionsBuilder.UseMySql("连接字符串");
改为:
optionsBuilder.UseLoggerFactory(MyLoggerFactory).UseMySql("连接字符串");
User-Agent 字符串不会进行更新以区分 Windows 11 和 Windows 10。
但我们可以使用 JS 进行客户端判断是否为 Windows 11:
navigator.userAgentData.getHighEntropyValues(["platformVersion"])
.then(ua => {
if (navigator.userAgentData.platform === "Windows") {
const majorPlatformVersion = parseInt(ua.platformVersion.split('.')[0]);
if (majorPlatformVersion >= 13) {
console.log("Windows 11 or later");
}
else if (majorPlatformVersion > 0) {
console.log("Windows 10");
}
else {
console.log("Before Windows 10");
}
}
else {
console.log("Not running on Windows");
}
});
支持客户端提示 User-Agent 浏览器:
浏览器 | 通过客户端提示 User-Agent 区别? |
---|---|
Microsoft Edge 94 及以上 | 是 |
Chrome 95 及以上 | 是 |
Opera | 是 |
Firefox | 否 |
Internet Explorer 11 | 否 |
首先,禁止网站下所有 .php 等文件均不允许被访问到。
在 nginx 网站配置文件中,include enable-php-**.conf; 上方插入:
location ~ ^/.*\.(php|php5|py|sh|bash|out)$ { deny all; }
其中,^ 匹配开始,/.* 匹配所有目录和文件名,\.(php|php5|py|sh|bash|out) 匹配文件后缀名,$ 匹配结束。
即便如此,仍然忽略了 nginx 中 .php 文件名后加斜杠仍然能访问到的情况,譬如我们访问这个网址:
https://xoyozo.net/phpinfo.php/abc.html
nginx 仍然运行了 phpinfo.php,给了后门可趁之机,所以改进为:
location ~ ^/.*\.(php|php5|py|sh|bash|out)(/.*)?$ { deny all; }
Discuz! X3.4 需要写入权限的目录:
/自研目录/upload/
/config/
/data/
/uc_client/data/
/uc_server/data/
/source/plugin/
但,这些都不重要,我们已经禁止了所有目录的 .php 访问了。
第二步,解禁需要直接被访问到的文件路径。
Discuz! X3.4 根目录下的 .php 文件都是入口文件,需要能够被访问到:
/admin.php
/api.php
/connect.php
/forum.php
/group.php
/home.php
/index.php
/member.php
/misc.php
/plugin.php
/portal.php
/search.php
其它目录中需要被直接访问到的文件:
/archiver/index.php
/m/index.php
/uc_server/admin.php
/uc_server/avatar.php
/uc_server/index.php
部分插件文件需要能够被直接访问到:
/source/plugin/magmobileapi/magmobileapi.php
/source/plugin/smstong/accountinfo.php
/source/plugin/smstong/checkenv.php
另外如果有自建目录需要有 .php 访问权限,那么也需要在此处加白,本文以 /_/ 目录为例
最终拼成:工具
location ~ ^(?:(?!(/admin\.php|/api\.php|/connect\.php|/forum\.php|/group\.php|/home\.php|/index\.php|/member\.php|/misc\.php|/plugin\.php|/portal\.php|/search\.php|/archiver/index\.php|/m/index\.php|/uc_server/admin\.php|/uc_server/avatar\.php|/uc_server/index\.php|/source/plugin/magmobileapi/magmobileapi\.php|/source/plugin/smstong/accountinfo\.php|/source/plugin/smstong/checkenv\.php|/_/.*\.php))/.*\.(php|php5|py|sh|bash|out)(/.*)?)$ { deny all; }
这里用到正则表达式中的“不捕获”和“负向零宽断言”语法,格式为:
^(?:(?!(允许的目录或文件A|允许的目录或文件B))禁止的目录和文件)$
值得注意的是,此句负向零宽断言中的匹配内容是匹配前缀的,也就是说
https://xoyozo.net/index.php
https://xoyozo.net/index.php/abc.html
都可以访问到,而
https://xoyozo.net/forbidden.php
https://xoyozo.net/forbidden.php/abc.html
都访问不到,这是符合需求的。
如果自建目录 /_/ 下有写入需求,单独禁止即可,以 /_/upload/ 为例:
location ~ ^/_/upload/.*\.(php|php5|py|sh|bash|out)(/.*)?$ { deny all; }
ThinkPHP 网站有统一的访问入口,可按本文方法配置访问权限。
特别注意:上面的代码必须加在 PHP 引用配置(include enable-php-**.conf;)的上方才有效。
判断是否为数字
if isNumeric(id) then
else
response.End
end if
强制转换为数字
ii = clng(ii)
转义单引号
replace(xxx, "'", "\'")
判断一个字符串是否包含另一个字符串
if instr(str, "s")>0 then
response.Write(1)
else
response.Write(0)
end if
Regex.Escape(String) 方法:
通过替换为转义码来转义最小的字符集(\、*、+、?、|、{、[、(、)、^、$、.、# 和空白)。 这将指示正则表达式引擎按原义解释这些字符而不是解释为元字符。
示例:
string str = @"123\c\d\e";
string r1 = @"\d";
string r2 = Regex.Escape(r1);
return Json(new
{
m1 = Regex.Matches(str, r1).Select(c => c.Value),
m2 = Regex.Matches(str, r2).Select(c => c.Value)
});
结果:
{
"m1":[
"1",
"2",
"3"
],
"m2":[
"\\d"
]
}
一般地,我们使用通配符 a*c 在字符串 abcd 中查找:
string s = @"abcd";
string w = @"a*c";
string r = Regex.Escape(w).Replace(@"\*", @".*?").Replace(@"\?", @".?");
return Content(Regex.Match(s, r).Value);
结果:
abc
同理,使用通配符 \d*\f 在字符串 \a\b\c\d\e\f 中查找:
string s = @"\a\b\c\d\e\f";
string w = @"\d*\f";
string r = Regex.Escape(w).Replace(@"\*", @".*?").Replace(@"\?", @".?");
return Content(Regex.Match(s, r).Value);
结果:
\d\e\f
测试在长度为 403 的字符串中查找,特意匹配最后几个字符:
string a = "";
for (int i = 0; i < 100; i++) { a += "aBcD"; }
a += "xYz";
string b = "xyz"; // 特意匹配最后几个字符
Stopwatch sw1 = Stopwatch.StartNew();
bool? r1 = null;
for (int i = 0; i < 10000; i++) { r1 = a.IndexOf(b) >= 0; }
sw1.Stop();
Stopwatch sw2 = Stopwatch.StartNew();
bool? r2 = null;
for (int i = 0; i < 10000; i++) { r2 = a.Contains(b); }
sw2.Stop();
Stopwatch sw3 = Stopwatch.StartNew();
bool? r3 = null;
for (int i = 0; i < 10000; i++) { r3 = a.ToUpper().Contains(b.ToUpper()); }
sw3.Stop();
Stopwatch sw4 = Stopwatch.StartNew();
bool? r4 = null;
for (int i = 0; i < 10000; i++) { r4 = a.ToLower().Contains(b.ToLower()); }
sw4.Stop();
Stopwatch sw5 = Stopwatch.StartNew();
bool? r5 = null;
for (int i = 0; i < 10000; i++) { r5 = a.Contains(b, StringComparison.OrdinalIgnoreCase); }
sw5.Stop();
Stopwatch sw6 = Stopwatch.StartNew();
bool? r6 = null;
for (int i = 0; i < 10000; i++) { r6 = a.Contains(b, StringComparison.CurrentCultureIgnoreCase); }
sw6.Stop();
Stopwatch sw7 = Stopwatch.StartNew();
bool? r7 = null;
for (int i = 0; i < 10000; i++) { r7 = Regex.IsMatch(a, b); }
sw7.Stop();
Stopwatch sw8 = Stopwatch.StartNew();
bool? r8 = null;
for (int i = 0; i < 10000; i++) { r8 = Regex.IsMatch(a, b, RegexOptions.IgnoreCase); }
sw8.Stop();
return Json(new
{
IndexOf_________________ = sw1.Elapsed + " " + r1,
Contains________________ = sw2.Elapsed + " " + r2,
ToUpper_________________ = sw3.Elapsed + " " + r3,
ToLower_________________ = sw4.Elapsed + " " + r4,
OrdinalIgnoreCase_______ = sw5.Elapsed + " " + r5,
CurrentCultureIgnoreCase = sw6.Elapsed + " " + r6,
IsMatch_________________ = sw7.Elapsed + " " + r7,
IsMatchIgnoreCase_______ = sw8.Elapsed + " " + r8,
});
结果参考:
{
"indexOf_________________": "00:00:00.1455812 False",
"contains________________": "00:00:00.0003791 False",
"toUpper_________________": "00:00:00.0038182 True",
"toLower_________________": "00:00:00.0026113 True",
"ordinalIgnoreCase_______": "00:00:00.0096550 True",
"currentCultureIgnoreCase": "00:00:00.1596517 True",
"isMatch_________________": "00:00:00.0053627 False",
"isMatchIgnoreCase_______": "00:00:00.0084132 True"
}