外观
实时推送
字数
2907 字
预计阅读
11 分钟
有些变化不用刷新页面就能看到:收到新的站内信、公告发布了、有新的审批待办、被管理员强制下线……这些都是服务端主动通知浏览器的,叫做实时推送。
它基于 WebSocket(一种服务端可以主动给浏览器发消息的连接方式),用开源的 Socket.IO 库实现。登录后,每个打开的标签页会自动和服务端建立一条连接,退出登录时自动断开,用户不需要做任何设置。移动端也有,见移动端。
哪些事件会即时送达
| 事件 | 谁会收到 | 页面上的变化 |
|---|---|---|
| 新的站内信 | 收件人 | 右上角通知铃铛的未读数立刻加 1,消息列表重新加载 |
| 在 系统管理 → 消息中心 → 通知公告 里发布、撤回公告,或删除已发布的公告 | 所有已登录的用户 | 铃铛里的公告列表重新加载 |
| 审批待办有变化:分到了新任务、任务办完了、被收回或转给了别人等 | 待办有变化的人 | 侧边菜单 流程审批 → 我的待办 上的数字、首页"工作概览"里的"我的待办"、打开着的"我的待办"列表,立刻更新 |
| 在 系统监控 → 在线用户 里点"强退" | 被强退的那次登录 | 提示"你已被管理员强制下线,请重新登录",然后跳到登录页 |
审批的各种通知(新待办、通过、驳回、抄送、催办、超时提醒等)会先写成站内信,所以也会让铃铛立刻更新,见工作流 · 通知。
退出登录、管理员重置密码、用户被停用或删除时,相关的连接也会立刻断开。本人修改密码时,除了当前这次登录,其他登录的连接都会断开。
推送只是"提醒"
- 内容仍然按权限读取。推送里只有编号、标题这类最少的信息,页面收到后再通过普通接口读取详情,权限和数据范围照常生效。所以推送不会让人看到本来看不到的东西;
- 不在线就收不到,也不会补发。不过站内信、公告、待办本身都保存在数据库里,下次打开页面或重新连上时会重新加载,内容不会丢。
断线和重连
- 网络中断、电脑休眠、服务端重启之后,会自动重连,间隔从约 1 秒开始,最长 5 秒;
- 连接断开期间,通知铃铛和待办数会改为每 60 秒查询一次(只在页面处于前台时)。重新连上、切回这个窗口、打开铃铛下拉时,也会立刻刷新一次;
- 断开的时间太长,登录凭证(访问令牌)已经过期了:会自动换一个新的再连。换不了(比如已经在别处退出了登录),就不再重连,下一次操作时弹出"会话已过期",可以选择重新登录或者留在当前页面;
- 被强制下线或者会话被结束后不会重连;
- 一次登录的连接最长保持到这次登录的最长期限:默认 12 小时,登录时勾选"保持登录"为 7 天。
安全
- 每次连接都要验证,包括每次重连:必须带着有效的、本系统自己的登录凭证(电脑端或移动端的登录)。第三方应用(OAuth2 客户端)拿到的令牌连不上;
- 浏览器发起的连接只接受本站页面,或者是环境变量
CORS_ORIGIN里列出的网站。这样别的网站无法借用户的登录状态偷偷建立连接(这种攻击叫"跨站 WebSocket 劫持")。脚本等非浏览器客户端一般不带来源信息,不做这项检查,只看登录凭证是否有效; - 只能服务端发给浏览器。浏览器不能通过这条连接发送任何指令。要提交数据,一律走普通接口,权限检查、参数校验、操作日志都照常生效;
- 推送的文字一律按纯文本显示,不会被当作网页代码执行。
更多会话安全措施见安全基线。
试一试:实时推送示例
系统工具 → 生成示例 → 实时推送示例 演示了实时推送的完整过程。可以开两个浏览器,用不同的账号登录,互相发消息。
页面分三部分:
- 实时连接:显示当前连接的状态,"已连接"、"重连中"或"已断开";
- 发送消息:发送对象可以选"指定用户"、"角色"或"全员",消息内容是纯文本,最多 500 字。发送后提示有几位在线用户收到(同一个人开了多个标签页只算一位);
- 接收日志:发给你的消息,最新的在最前面,保留最近 100 条。切换到其他标签页再回来,日志还在;关闭这个标签页后清空。
发送的规则:
- 按用户或角色发送时,只发给你数据范围内的、启用的用户。范围外的人会被直接忽略,不会报错;停用的角色没有成员;
- 消息原样显示,写
<b>粗体</b>也不会变成粗体; - 每个 IP 每分钟最多发送 30 次,每次发送都会记入 系统管理 → 日志管理 → 操作日志。
| 按钮 | 权限 | 说明 |
|---|---|---|
| 发送 | demo.realtime.send | 发给指定用户或角色 |
| 发送给全员 | demo.realtime.broadcast | 发给所有已登录的用户。同时还需要"发送"权限 |
谁能用这个示例
默认只有超级管理员能用。要给其他人用,在 系统管理 → 角色管理 里给角色分配这个菜单和上面的按钮。
部署注意
- 反向代理(比如 nginx)要把
/socket.io/按 WebSocket 方式转发,并把浏览器访问的地址原样告诉服务端,否则连接会被拒绝。具体配置见在代码中推送实时消息 · 反向代理和部署; - 前端如果单独部署在另一个域名,要把那个地址写进服务端的环境变量
CORS_ORIGIN; - 只用 WebSocket 传输,负载均衡不需要"会话粘滞"(同一个用户的请求固定发到同一台服务器);
- 微信小程序要在微信公众平台的"开发管理 → 开发设置 → 服务器域名"里,把
wss://<API 域名>加入 socket 合法域名。不配也能用,只是角标退回每 60 秒查询一次; - 手机上的角标总要等约 60 秒才变时,先查上面两项(socket 合法域名、反向代理的 WebSocket 转发)。再查连接是不是因为来源网站不被允许而被拒绝:真机的连接可能带着来源网址(
Origin),它既不是本站、也不在CORS_ORIGIN里时会被拒绝。可以在反向代理上临时记录/socket.io/请求的Origin,把看到的那个值原样加进CORS_ORIGIN(只加这一个,不要放宽),再重启服务端。注意CORS_ORIGIN同时也决定哪些网站可以凭登录 Cookie 换新的访问令牌。
目前只支持单实例
模板 v1 按单实例部署设计。如果同时运行多个服务实例:
- 推送只能送到连在同一个实例上的用户,连在其他实例上的人收不到;
- 示例页的"几位在线用户收到"只统计本实例,会偏少;
- 强制下线时,连在其他实例上的连接不会马上断开,要等它下一次重连时被拒绝,或者到登录的最长期限才断开。
需要多实例时,要自己加上 Socket.IO 的 Redis 适配器(模板没有内置),方法见在代码中推送实时消息 · 多实例,另见部署 · 单实例说明。
移动端
移动端(uni-app,微信小程序、安卓、iOS)也有实时连接,连的是同一个服务端,规则和电脑端相同:
| 事件 | 手机上的变化 |
|---|---|
| 审批待办有变化 | 底部"审批"页签的角标、工作台的待办数和"待我审批"列表重新加载 |
| 新的站内信 | 底部"消息"页签的角标、工作台的未读消息数重新加载 |
| 被管理员强制下线 | 回到登录页,提示"你已被管理员强制下线,请重新登录" |
公告发布、撤回的推送,手机收到后不做处理:"消息"页每次打开都会重新加载。
- 什么时候连接:应用回到前台、打开底部的任一页签时连接,并立刻重新读取一次待办数和未读数,补上不在线期间的变化;
- 什么时候断开:应用切到后台、登录、退出登录时断开。小程序弹出选图片、订阅消息这类系统窗口时也会短暂切到后台,回来后自动重连;
- 连不上时:比如小程序没有配置 socket 合法域名,或者网络不允许。底部页签显示期间,待办数和未读数每 60 秒查询一次,所以角标最迟 60 秒内更新。
在开发调试用的 H5 页面上,角标在推送后 2 秒内更新。真机上取决于网络和配置,不保证一定这么快,最坏情况由 60 秒的查询兜底。
开发指南
- 在代码中推送实时消息:服务端怎么发、前端怎么订阅、使用规则、测试和部署
- 消息通知:必须送达的内容,先发站内信,推送会自动跟上
- 服务 · 提交之后才能做的事
- 防重复提交、限流与锁:多实例时限流计数也要改用 Redis
- 相关功能:工作流、安全基线、权限与数据范围