ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ROS Web控制台安全加固:Nginx+JWT双重认证实战

ROS Web控制台安全加固:Nginx+JWT双重认证实战

简介:这套资源面向网络管理员与RouterOS学习者,整合了ROS Web认证所需的完整热点页面素材与配置文件。针对公共场所无线网络接入认证场景,涵盖登录页、错误页、重定向页、注销页等HTML模板,以及配套的CSS、JavaScript和图片资源,能够帮助读者快速搭建或定制RouterOS热点认证界面,理解认证流程中的页面交互逻辑。

资源共71个文件,包括19个HTML页面、21张JPG图片、18张PNG图片、8张GIF动图,以及CSS、JS、XSD等辅助文件,压缩包约95KB,结构紧凑、便于部署。其中登录、重定向、状态展示等关键页面一应俱全,可据此对照ROS Hotspot配置进行调整,也可直接替换为品牌化页面投入使用。已有306人学习下载,适合需要部署公共Wi-Fi认证、定制登录界面的网络运维人员,以及希望借助实际页面理解热点认证机制的初中级ROS学习者。

1. ROS WEB认证:为什么你的ROS小车控制台像个裸奔的靶子

最近给一台ROS小车做远程控制,用户要求在客厅用浏览器看摄像头画面、点方向键控制。搭控制台很快:rosbridge_suite 转发话题,web_video_server 推图像流,浏览器端一个 Canvas 就完事。真正花时间的是认证——默认状态下,同一局域网里只要有人能访问到 IP 和端口,谁都能订阅画面、往 /cmd_vel 推指令,把正在运行自主导航的小车原地调头。这个坑很多人做完测试才发现。我整理的这套 ROS WEB认证方案,把登录签发、Nginx 转发校验、ROS 侧话题网关串起来,覆盖了从网页登录到指令落地的完整链路。适合正在给 ROS 小车做 Web 控制台、或者做远程监控的工程师,也适合刚学 ROS 想理解 Web 通信安全的入门者。

2. 把认证前置到 Web 层:Rosbridge 协议、Nginx auth_request 与 JWT 选型

2.1 ROS Web 控制的默认链路:Rosbridge + WebSocket,缺什么

先看绝大多数 ROS Web 控制台是怎么跑起来的。机器上启动一个 rosbridge_server,默认监听 9000 端口,浏览器通过 WebSocket 发 JSON 指令,例如订阅 /odom、发布 /cmd_vel。再启动 web_video_server,监听 8080,直接把图像 topic 编码成 MJPEG 流,浏览器用 img 标签就能看。

# 机器人主控上通常这两条命令 roslaunch rosbridge_server rosbridge_websocket.launch rosrun web_video_server web_video_server _port:=8080

不管环境是用鱼香ROS一键装的还是手动装的,这两条命令都能跑。链路本身很成熟,但这里没有身份概念。rosbridge_server 不关心你的来源 IP,不校验任何用户名密码,也不检查你发来的 topic 有没有权限。它只负责把 WebSocket JSON 转成 ROS 话题和服务调用。所以只要你的机器人网段里有另一台电脑,那位用户用几条命令,或者干脆打开浏览器控制台,就能做任何 Web 端能做的事。

有人觉得“路由器或防火墙把端口只对可信电脑开放”就能解决,我在项目初期也这么干过。但路由器上的访问控制在多人办公区、访客 WiFi、手机热点接入这些环境下很快失效——你不可能天天改路由器,也不可能保证每个拿着手机的人都不会连到你运行中的机器人。所以认证必须做成应用层的东西,和你的 ROS 节点耦合在一起。

2.2 为什么选 Nginx auth_request 而不是改 rosbridge 源码

直接改 rosbridge_server 源码,给每个 WebSocket 连接加登录校验?这条路不是不能走,但以后每次升级 rosbridge 都要把补丁重提一遍,遇到 bug 还得自己修。工程上不划算。我选择用一个 Nginx 的 auth_request 模块把校验逻辑架在 rosbridge 前面,理由有三个:

  • auth_request 会对每一个进入的 HTTP/WebSocket 请求,先转发到一个内部 /auth/verify 接口,接口返回 200 才放行;
  • ROS 这套 Web 工具本来就要走 Nginx 做公网域名的 443 端口转发,顺手在同一份配置里加校验,少一层运维复杂度;
  • JWT 是纯字符串,不依赖 ROS master,用户服务完全独立,后续接 LDAP 或者手机验证码都不需要动 ROS 端。

Nginx 在这里做的是把“转发”和“校验”两个动作分开。你可以把它理解成一个门卫:所有访问先到门卫这,门卫拿你的令牌去问认证服务,认证服务点头才放进 rosbridge 的房间。这也是整个 ROS WEB认证方案里最关键的一层:把“有没有资格访问”和“能不能碰某个话题”拆成两个独立问题,前者在 Web 层解决,后者放到第 4 章的 ROS 侧解决。

2.3 认证状态机:登录、颁发 JWT、握手校验、过期续约

直接贴我在用的状态流转,这也是后续部署的骨架:

阶段动作默认参数
登录POST /auth/login用户名+密码,返回 access_token + refresh_token
建立连接携带 access_token 连接 WebSocket用 URL query 参数传递
握手校验Nginx auth_request 调 /auth/verify2xx 通过,401 拒绝
正常通信WebSocket 长连接内继续发话题不再打断
过期重连access_token 过期后前端重建 WebSocketaccess 10 min,refresh 24 h

JWT 的 exp 我设得比较短,10 分钟,因为 WebSocket 握手只有一次,token 过期后已建立的连接不会被自动断开,短 exp 能减少泄漏后的攻击窗口。refresh_token 存放在前端内存或 sessionStorage 里,有效期 24 小时,前端收到 401 后立刻用 refresh_token 换新的 access_token,然后重建 WebSocket。这样浏览器长期挂着控制页面,人不在时新连接会自动建立,使用者不会感知到掉线。

这里需要说明一个边界:auth_request 只在 WebSocket 升级握手时校验一次,升级之后的数据帧 Nginx 不会再逐个校验。所以第 4 章还要在 ROS 侧加一层话题网关,两者配合,而不是指望 Nginx 一把梭。如果只做连接级认证,不做话题级授权,这套方案的理论基线就是“能连上就能操作”。

在 Nginx 配置里,核心是这一段,先把意图看懂,第 3 章会给完整配置:

location /rosbridge { auth_request /auth/verify; proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }

逻辑说明:所有走到 /rosbridge 的请求,先由 auth_request 发一个内部子请求给 /auth/verify,子请求返回 200 之后,Nginx 才把原始请求转发到本机 9000 端口的 rosbridge_server。对 WebSocket 来说,这个校验发生在握手阶段,恰好是建立连接前的最后一道闸。参数说明:proxy_http_version 必须设定为 1.1,否则 WebSocket 的 Upgrade 头会被 Nginx 丢掉,后面连接直接失败。

3. 落地第一步:用户服务、Token 签发与 Nginx 转发规则

3.1 建一个最小 Flask 用户服务,签发 JWT 与刷新 Token

先做核心代码。下面这个 Flask 服务只保留认证逻辑,生产环境建议把用户名密码挪到数据库,或者直接对接公司已有的账号体系。

# auth_service.py from flask import Flask, request, jsonify import jwt, datetime, os, uuid SECRET = os.getenv("JWT_SECRET", "dev-secret-change-me") ACCESS_MIN = 10 REFRESH_HOURS = 24 app = Flask(__name__) USERS = { "admin": {"password": "admin123", "role": "admin"}, "view": {"password": "view123", "role": "viewer"}, } def make_tokens(username, role): access = jwt.encode( {"user": username, "role": role, "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=ACCESS_MIN)}, SECRET, algorithm="HS256") refresh = jwt.encode( {"user": username, "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=REFRESH_HOURS)}, SECRET, algorithm="HS256") return access, refresh @app.post("/auth/login") def login(): data = request.get_json() user = USERS.get(data.get("user", "")) if not user or user["password"] != data.get("password"): return jsonify({"error": "bad credentials"}), 401 access, refresh = make_tokens(data["user"], user["role"]) return jsonify({ "access_token": access, "refresh_token": refresh, "role": user["role"], "client_id": uuid.uuid4().hex }) @app.post("/auth/refresh") def refresh(): data = request.get_json() try: payload = jwt.decode(data["refresh_token"], SECRET, algorithms=["HS256"], leeway=30) except jwt.PyJWTError: return jsonify({"error": "invalid refresh"}), 401 access, _ = make_tokens(payload["user"], USERS[payload["user"]]["role"]) return jsonify({"access_token": access})

逻辑说明:登录时校验用户名密码,签发 access_token 和 refresh_token。refresh 接口不校验密码,只看 refresh_token 本身有没有过期,所以 refresh_token 一定不能泄露到非浏览器环境。client_id 每次登录生成一个十六进制串,后面 ROS 侧网关就是靠它判断这个连接是哪一次登录来的。

参数说明:ACCESS_MIN 是访问令牌有效期,我设 10 分钟。如果控制页长期不刷新也不操作,过期后前端需要走一次 refresh 才能重建 WebSocket。REFRESH_HOURS 设 24 小时,超过一天用户要重新登录,对室内机器人这个窗口足够。JWT 用 HS256,SECRET 放在环境变量里,不要写死进 Git 仓库。

3.2 配置 Nginx:先校验再转发

把上面的服务接到 Nginx。完整配置如下,我直接在 server 块里写重点:

server { listen 443 ssl; server_name robot.local; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; location = /auth/verify { internal; proxy_pass http://127.0.0.1:5000/auth/verify; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; } location /auth/ { proxy_pass http://127.0.0.1:5000; } location / { auth_request /auth/verify; proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } location /video/ { auth_request /auth/verify; proxy_pass http://127.0.0.1:8080; proxy_buffering off; } }

逻辑说明:location /auth/ 是公网可访问的登录和刷新接口;/auth/verify 是 internal 子请求,只能由 Nginx 内部调用,外部访问不到。主链路 location / 对所有请求先发一个子请求到 /auth/verify,子请求返回 200 才把请求转到本机 9000 端口的 rosbridge_server。proxy_read_timeout 和 proxy_send_timeout 设得比较长,防止 WebSocket 长时间没消息被 Nginx 直接断开。最后单独加了 /video/ 用于 web_video_server 的摄像头流,同样走 auth_request,这样没登录就看不到画面。

参数说明:proxy_pass_request_body off 让 verify 子请求不携带原始请求体,减少内部流量。X-Original-URI 把原始地址带过去,方便你在 Flask 里做更细的路径校验。proxy_set_header Upgrade 和 Connection "upgrade" 是 WebSocket 能打通的关键,漏一条都会在握手时报 400。proxy_buffering off 对 MJPEG 流很重要,否则图像帧会被 Nginx 缓存积压,画面延迟越来越大。

3.3 前端登录到建立 WebSocket 的完整时序

前端部分我用原生 JavaScript 写,方便移植到 Vue 或 React。重点不是 UI,而是 token 存哪里、什么时候重建连接。

async function loginAndConnect() { const resp = await fetch('/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user: 'admin', password: 'admin123'}) }); const data = await resp.json(); sessionStorage.setItem('access_token', data.access_token); sessionStorage.setItem('refresh_token', data.refresh_token); sessionStorage.setItem('client_id', data.client_id); sessionStorage.setItem('role', data.role); connectRos(); } async function connectRos() { const token = sessionStorage.getItem('access_token'); const clientId = sessionStorage.getItem('client_id'); const ws = new WebSocket(`wss://${location.host}/rosbridge?token=${token}&c=${clientId}`); ws.onclose = async (e) => { if (e.code === 4001 || e.code === 1006) { await refreshAndReconnect(); } }; } async function refreshAndReconnect() { const refresh = sessionStorage.getItem('refresh_token'); const resp = await fetch('/auth/refresh', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({refresh_token: refresh}) }); if (resp.status !== 200) { window.location.href = '/login.html'; return; } const data = await resp.json(); sessionStorage.setItem('access_token', data.access_token); connectRos(); }

逻辑说明:登录后把 token 和 client_id 放在 sessionStorage,页面关闭自动清空,不会留在磁盘里。连接时把 token 和 client_id 拼进 WebSocket URL,Nginx 的 auth_request 拿到 request_uri 后会去校验 token。如果连接被后端拒绝,onclose 里会先走 refresh 再重连;如果 refresh 也失效,就直接跳回登录页。

参数说明:wss 表示走 TLS,如果只在内网测试,可改成 ws 和 80 端口。WebSocket URL 里的 c 参数是 client_id,会出现在 Nginx 日志里,所以不要在里面放密码。onclose 里判断 4001 是第 4 章话题网关拒绝连接时用的,如果 ROS 侧不接受这个 client_id,前端即使刷新 token 也没用。

4. 授权到话题:让 ROS 端知道“这个 token 能碰哪几个 topic”

4.1 为什么只信任 Nginx 不够

Nginx 的 auth_request 只在 WebSocket 握手时校验,一旦连接成功,之后所有 topic 数据帧都不再逐帧校验。这意味着如果合法用户的 token 泄露,或者用户权限被收回,已经建立的连接仍然能继续发 /cmd_vel,直到他主动关页面。对室内小车来说,这不够。

另一个问题是:Nginx 只能判断“这个连接是否持有有效 JWT”,它不知道这个 JWT 属于 admin 还是 viewer。如果你想做“访客只能看摄像头,管理员才能发指令”的粒度控制,必须在 ROS 侧加一道闸门。我采用的做法是:不让浏览器直接往 /cmd_vel 发,而是先发到 /web/cmd_vel,由 ROS 侧一个叫 topic_guard 的节点检查消息里携带的 client_id 和 role,再转发到真正的 /cmd_vel。相当于把 ROS 话题入口收窄成一个带门卫的窄门,这也是我认为 ROS WEB认证里最容易被跳过、但最值得做的一层。

4.2 用 topic_guard 节点做二次校验

先定义控制消息协议。这里不新增 .msg 文件,直接用 std_msgs/String,把 JSON 字符串作为载荷,前端和 ROS 端都容易扩展。这样做的好处是将来给控制消息加字段时,不用重新编译 ROS 消息。

前端发布代码:

ws.send(JSON.stringify({ op: "publish", topic: "/web/cmd_vel", msg: { data: JSON.stringify({ client_id: sessionStorage.getItem("client_id"), role: sessionStorage.getItem("role"), twist: { linear: {x: 0.2}, angular: {z: 0.1} } })} }));

ROS 侧 gate 节点:

#!/usr/bin/env python3 import rospy, json from std_msgs.msg import String from geometry_msgs.msg import Twist class TopicGuard: def __init__(self): self.cmd_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1) self.sub = rospy.Subscriber('/web/cmd_vel', String, self.cb) self.active_clients = {} rospy.Subscriber('/auth/clients', String, self.sync_clients) def sync_clients(self, msg): self.active_clients = json.loads(msg.data) def cb(self, msg): try: payload = json.loads(msg.data) except Exception: return cid = payload.get('client_id') role = self.active_clients.get(cid) if role is None: rospy.logwarn('unknown client_id %s', cid) return if role != 'admin': rospy.logwarn('client %s has no write permission', cid) return twist = Twist() twist.linear.x = float(payload['twist']['linear']['x']) twist.angular.z = float(payload['twist']['angular']['z']) self.cmd_pub.publish(twist) if __name__ == '__main__': rospy.init_node('topic_guard') TopicGuard() rospy.spin()

逻辑说明:guard 节点订阅两个入口,一个是 /web/cmd_vel 控制消息,一个是 /auth/clients 的活跃客户端列表。收到指令后,先解析 client_id,去 active_clients 里查角色,只有存在且是 admin 才拼 geometry_msgs/Twist 发布到 /cmd_vel。这样即使有用户绕过 Nginx 直接用 rosbridge 连到 /web/cmd_vel,没有 client_id 或角色不对也被挡在门外。

参数说明:active_clients 列表由用户服务维护,每次登录、刷新、过期都重新推送。这里的 JSON 协议很轻,TopicGuard 不需要知道 JWT 具体怎么签,只认 client_id 和 role,天然把认证服务与 ROS 端解耦。注意 /cmd_vel 的 Publisher 和 /web/cmd_vel 的 Subscriber 最好都用 queue_size=1,避免控制指令积压造成延迟。

4.3 用户服务如何维护活跃客户端列表

在 Flask 服务里,登录成功后除了返回 token,还要把 client_id、role、过期时间写入内存,然后通过 ROS 节点广播。这里用最直接的方式:在 auth_service 里调用 rospy,每次更新后 publish 到 /auth/clients。前提是 auth_service 和 TopicGuard 跑在同一台机器人上或同一个 ROS master 下,这是最常见的部署形态。

# 伪代码,示意广播时机 import rospy, json from std_msgs.msg import String rospy.init_node('auth_state') auth_clients_pub = rospy.Publisher('/auth/clients', String, queue_size=10) def broadcast_clients(): clients = {cid: role for cid, role, exp in sessions.values() if exp > now} auth_clients_pub.publish(String(json.dumps(clients)))

逻辑说明:每次 login、refresh、logout 或 token 过期清理时调用一次 broadcast_clients()。TopicGuard 收到消息后整体替换内存表,不需要逐条增量同步,简单可控。如果 auth_service 不跑在 ROS 环境里,改成让 TopicGuard 定时 HTTP 拉取也可以,只是多一个 HTTP 客户端依赖。

参数说明:这里把 session 全部放内存,服务重启就丢。对机器人场景,重启后所有浏览器需要重新登录,这是可以接受的。如果想不丢,可以挂 Redis,但工程复杂度会明显上升。实际项目里我更倾向内存方案,因为机器人重启本身通常意味着用户不在场,重新登录反而是合理行为。

4.4 不只是 cmd_vel:把同样的模式扩展到自主导航和其他话题

大多数 Web 控制台不只有 /cmd_vel。比如你想让管理员能调用 /move_base/goal 让小车自主导航,访客只能看画面。用同样的窄门模式:浏览器往 /web/goal 发 JSON,gateway 节点校验后再调用 actionlib 或者发布到 /move_base/goal。套路一致:先入窄门,校验 client_id,再调用真实的 ROS 接口。

但要注意,不要给每个话题单独写一个节点,最好用一个 guardian 节点统一管理所有 Web 入口。这样只维护一份 active_clients,不会出现多个节点内存表不一致的问题。在 ROS 多机通信配置里,guardian 节点跑在小车主控上,auth_service 可以跑在另外一台工控机上,只要它们属于同一个 ROS master,同步逻辑就不变。

5. 避坑与排查:ROS Web 认证里最常见的 5 个坑

5.1 现象:WebSocket 握手返回 403,控制台连不上

现象:浏览器控制台输出 WebSocket connection failed,Nginx error log 能看到 auth_request 子请求返回 401。

原因:最常见的是前端把 token 放在了 Authorization 头,而 auth_request 子请求没有透传这个头;另一种是浏览器发起的 OPTIONS 预检请求被 auth_request 拦了,返回 401,WebSocket 握手直接失败。这两个问题会在第一次接前端时集中爆发。

解决:前端把 token 放到 URL query 而不是 Header,因为同源部署下 URL 参数最简单。Nginx 里再补一条 OPTIONS 放行规则,让预检请求直接通过。改完后用 curl 模拟握手,看到 101 Switching Protocols 就算通了。

5.2 现象:连接能建立,但每 60 秒左右自动断开

现象:控制页打开后正常操作没问题,一旦停手超过一分钟,WebSocket 自动 close,前端重连后又能用一会儿。

原因:Nginx 的默认 proxy_read_timeout 是 60 秒。ROS 话题频率低时,WebSocket 数据帧可能超过 60 秒没有数据,Nginx 主动断开空闲连接。Rosbridge 自己不会发心跳包,所以这个问题几乎必现。

解决:在 Nginx location 里设置 proxy_read_timeout 3600s,proxy_send_timeout 3600s。如果你对安全性有额外要求,可以在前端加一个 30 秒的 ping 定时器,定时发一个空操作保持活跃,避免所有靠超时续命的隐藏逻辑。

5.3 现象:ROS 端 /cmd_vel 没有消息,前端 publish 却显示成功

现象:前端 onmessage 没有报错,但 guard 节点没打印任何日志,rostopic echo /cmd_vel 没反应。

原因:十有八九是话题名冲突。比如前端发布到 /web/cmd_vel,guard 订阅的是 /ctrl/cmd_vel,或者多个 launch 文件里对话题做了 remap。另一种是前端发布的 msg 不是 std_msgs/String 的 data 结构,而是直接发了一个 Twist,guard 的 json.loads 直接抛异常被 return 吞掉。

解决:先用 rostopic echo /web/cmd_vel 看消息是不是 JSON 字符串,再确认 guard 的订阅名和前端发布名完全一致。如果要保持代码健壮,cb 里用 try-except 是必要的,但调试时一定要把异常打出来,不要裸吞。

5.4 现象:viewer 角色也能发控制指令

现象:用 view 账号登录,前端把 role 改成了 admin,居然也能让小车动起来。

原因:如果只用 JWT 里的 role 做判断,而 JWT 是前端自己组装的,那就等于没做授权。前端 sessionStorage 里的 role 只是展示层用的,真正的角色必须由服务端根据 client_id 维护。TopicGuard 的 active_clients 里存的是服务端下发的角色,前端改 sessionStorage 里的 role 不影响服务端状态。

解决:登录接口返回的 role 写入 auth_state 后,guard 里严格判断 active_clients 中的角色。如果发现前端篡改,guard 不要报错,直接丢弃指令并打一条 warning 日志,便于排查。

5.5 现象:自签名证书 Firefox 能连,Chrome 死活连不上

现象:Nginx 配了自签名证书,Firefox 手动信任之后能正常用,Chrome 始终报 ERR_CERT_AUTHORITY_INVALID,清缓存、重启都没用。

原因:Chrome 对证书的 SAN 扩展校验很严格。如果用 IP 访问,证书里必须包含该 IP 的 subjectAltName;如果用主机名访问,证书里必须包含 DNS 项。早期自签名工具生成的证书只写了 CN,没有 SAN,Chrome 直接拒绝。

解决:用 OpenSSL 生成证书时加上 -addext "subjectAltName=IP:192.168.1.10,DNS:robot.local"。开发期更推荐用 mkcert 一键生成受信任的本地证书,能省不少时间。生成后要重新加载 Nginx,再用浏览器无痕模式验证。

6. 进阶落地:自签名 HTTPS、多机通信与认证自检脚本

6.1 用自签名证书让浏览器信任本地机器人

局域网机器人没有公网域名,自签名证书是常态。建议建一个自己的 CA,用 CA 给每台机器人签发证书。开发时用 mkcert 最省事:

mkcert -install mkcert robot.local 192.168.1.10 cp robot.local+1.pem /etc/nginx/certs/server.crt cp robot.local+1-key.pem /etc/nginx/certs/server.key

生成的证书会在 Chrome 和 Firefox 里直接受信任,不再跳警告。生产环境可以把 mkcert 的根证书导入到每台需要访问的用户电脑,这样访客手机也一样。注意证书 SAN 里必须同时写 DNS 和 IP,不然换种方式访问就失效。

6.2 ROS 多机通信场景下的认证链

如果 auth_service 跑在另一台工控机上,TopicGuard 跑在机器人上,需要先配好 ROS 多机通信。常见做法是设置 ROS_MASTER_URI 指向小车主控,ROS_IP 指向各自网卡地址。但不要为了“方便远程调试”把 ROS master 直接暴露到公网,这等于把整个认证方案绕过去了。公网上只留 Nginx 的 443 端口,ROS 侧所有话题都只在可信内网流动。多机通信时 /auth/clients 话题会跨机器广播,请确认该话题只存在于你信任的主机之间。

6.3 一键自检脚本

每次改动配置后,我习惯跑一遍自检,确认没有把 viewer 读写权限放错:

#!/bin/bash # check_web_auth.sh curl -s -X POST http://robot.local:5000/auth/login \ -H 'Content-Type: application/json' \ -d '{"user":"view","password":"view123"}' | jq .role # 检查 token 过期后是否还能发指令 # 用未登录的 ws 客户端连接,预期被 Nginx 拒绝 python3 - <<'PY' import asyncio, websockets async def test(): try: async with websockets.connect("ws://robot.local/rosbridge"): print("FAIL: 未认证连接成功了") except Exception: print("PASS: 未认证连接被拒绝") asyncio.run(test()) PY

这个脚本能最快确认认证层是否生效。第一段检查 viewer 登录返回的角色是不是 viewer,第二段验证未登录连接被 Nginx 挡掉。日志里看到 PASS 再去做真车测试,能省掉大量来回排查时间。

第一次做这个项目时,我把 view 账号误放到了 admin 组,演示前半个小时才发现话题网关一直放行 viewer 的指令。从那以后我每次给 ROS 小车加 Web 认证,都强制跑一遍自检脚本,确认 viewer 发不出去、token 过期后重连正常,再上真机。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进