
简介面向需要在 RouterOSROS中部署 Web 认证热点的网络管理员这份压缩包提供了一整套可直接使用的 ROS hotspot 认证页面模板与配套素材。包含登录、认证成功、注销、错误、重定向等完整流程页面以及用于页面样式与脚本调用的 CSS、JS 文件还配有多种格式的图片和小动画可据此快速替换默认认证界面加入品牌标识或服务条款实现更规范的热点登录体验。资源共 71 个文件其中以 HTML 页面19 个和 JPG/PNG/GIF 图片素材为主另有少量 txt 说明、CSS/JS/XSD 配置文件压缩包整体仅 95KB便于直接导入 ROS 的 Files 目录进行覆盖或修改。对这些页面和脚本进行简单定制即可理解热点认证在 AL/SL/FL 等状态下的跳转逻辑并掌握 login.html、logout.html、error.html 等关键模板的作用。已有 306 人学习下载适合具备一定 ROS 基础、希望从零搭建或美化 Web 认证入口的网络运维者和技术爱好者。1. ROS Web认证到底是什么先回答“谁有资格动这台机器人”ROS Web认证听起来像是“给ROS网页加个登录框”但真正要解决的是浏览器从Web页面向机器人发指令时中间隔着 HTTP、WebSocket、rosbridge 多层通道每一层都需要确认“是谁在发指令、有没有权限发这条指令”。我经手的某室内巡检机器人项目下称模拟项目X第一次暴露Web控制台时局域网内任何一台电脑都能通过浏览器让底盘电机转起来那一刻我就确认了“登录页”只是面子工程ROS Web认证要解决的是从浏览器到ROS节点的完整信任链。这套方案面向的不是“做个Demo给自己看”的场景而是给多台机器人加共享Web接口、无人值守站点、或者把机器人控制能力开放给内部系统的从业者。难点不在于签发一个Token而在于Token要穿过Nginx网关、WebSocket握手、JSON-RPC消息三个环节每个环节都有各自的坑。接下来我会按“先选层、再落地、后排错、最后提权”的顺序把这套链路完整讲透。2. 先想清楚ROS Web认证到底该做在哪一层先说一个结论不要一上来就在每个ROS节点里加鉴权函数那样会把一个认证问题拆成几十个打补丁的问题。ROS Web认证面对的是“谁能用Web控制机器人”这个边界天然在ROS通信之外所以大多数情况下应该把认证做在流量进入ROS之前的路径上。ROS侧常见的Web接入方式有两种一种是网页直接调用机器人上的HTTP接口比如查询电池、地图、日志另一种是通过 rosbridge_server 提供 WebSocket 通道网页订阅话题、发布控制指令、调用服务。两条路径的流量都可以在进入ROS之前由一道统一认证网关拦截。2.1 为什么我建议从网关层入手而不是第一天改ROS节点ROS节点内部做鉴权往往意味着每个订阅、发布、服务回调里都要插入身份判断代码。更重要的是ROS节点数量和类型会影响鉴权逻辑的一致性你给底盘节点加了鉴权导航节点、地图节点、日志节点也都要加一遍而且每台的ROS版本和通信机制还有差异。网关层则完全不同。它把认证收敛到一个入口所有HTTP请求和WebSocket握手都必须经过它。节点层代码零改动机器人跑多长演进不慌。网关层的代价是控制粒度比较粗——它只能判断“这个请求是否来自已登录的人”不能判断“这个人能不能发布 /cmd_vel”。但这句话反过来也成立网关层能挡住绝大多数匿名访问和粗暴扫描先把底兜住。我一般会这样分层用网关层做基础认证挡住外来流量在关键控制话题上再做一层消息级鉴权防止内部账号越权。两层配合认证是前提授权是深度控制。很多项目死在第一天就想做“完美的细粒度授权”结果连基础登录都没跑通。2.2 三层选型对比把控粒度、改动量与落地成本放到一张桌上体现实操选型建议直接把选项列成对照表按自家需求勾选方案改动量控制粒度WebSocket友好度典型适用场景Nginx/网关层校验小只需配置和独立auth服务粗只能控制到路由级别中WebSocket握手可拦截长连接建立后不再校验快速封住匿名访问保护地图、日志等HTTP接口rosbridge消息代理/插件中需要在消息通道上做过滤细可以精确到每个 topic 和 service高直接在长连接内逐条消息鉴权需要控制谁能发布 /cmd_vel、谁能订阅 /mapROS节点内校验大每个节点都要改最细可以结合业务逻辑判断低主要靠节点自身防御安全要求极高、节点本身就需要独立权限体系决策逻辑可以简化成三步如果机器人还处于产品验证期先上网关层成本最低如果确认存在多角色使用场景比如操作员和访客看同一个地图但只有操作员能发指令再加一层消息代理节点内校验可以等业务功能稳定后再补不要一开始就铺开。2.3 Token载荷设计别人拿到的JWT里到底应该有什么选定网关层方案后Token本身的设计要提前想清楚。我见过不少项目把JWT当“临时通行证”只放user_id和exp后面想加权限的时候才发现所有客户端都过期不了只能等token自然失效非常被动。机器人场景我建议载荷至少包含这几项{ iss: robot-web-auth, sub: operator-001, aud: robot-gateway, exp: 1713000000, iat: 1712996400, scope: cmd_vel:pub;map:sub;battery:sub }逻辑说明iss标识签发方方便多机器人集群时快速知道token来源sub是操作员ID也是后续追踪操作行为的核心字段aud告诉网关这张token是发给哪个机器人网关的exp和iat是一对必须同时有没有iat的话刷新机制很难做scope是我最看重的字段它不等同于“角色”而是直接列出允许访问的话题和操作这样后续消息代理做话题级授权时可以直接解析它不用再拉一次数据库。签名算法上的建议是单台机器人、单网关用 HS256 足够如果未来会有多台网关、或者需要独立鉴权服务器统一验签再上 RS256。RS256 的优势是私钥只在签发端网关只持有公钥泄露一个网关不会拖垮整个签发体系缺点是密钥管理复杂度更高。小项目别一上来就上RS256先把HS256的密钥保管好比什么都强。注意HS256的密钥一旦泄露等于任何人都能伪造token。密钥不要写死在代码里放到环境变量或者机器人本机单独的配置文件里并且保证只有运行机器人服务的账号能读。3. 在Nginx网关层跑通JWT校验auth_request的最小可运行配置选好方案之后最直接的落地方式就是Nginx的auth_request模块。它的逻辑很简单收到外部请求后先发一个内部子请求给你指定的鉴权接口接口返回200就放行返回401或403就拒绝然后把鉴权接口返回的一些头信息透传给上游业务服务。这一节我会给出可以照抄的配置。3.1 先搭一个能签发Token的登录接口FastAPI最小实现登录接口和鉴权接口本可以在同一个服务里但建议拆成两个路径/api/login只在登录时调用/api/authz只给Nginx子请求调用这样日志可以分别追踪。# token_auth.py —— ROS Web认证的最小FastAPI实现 from fastapi import FastAPI, Header, HTTPException import jwt, time, os app FastAPI() SECRET os.environ.get(ROBOT_JWT_SECRET, change-me-in-production) app.post(/api/login) def login(payload: dict): # 机器人没有完整账号体系时以操作员ID口令代替 operator_id payload.get(operator_id) secret payload.get(secret) if secret ! os.environ.get(ROBOT_OPERATOR_SECRET, op-placeholder): raise HTTPException(status_code401, detailbad secret) now int(time.time()) token jwt.encode( { sub: operator_id, iat: now, exp: now 7200, # 2小时过期兼顾操作时长与安全 scope: cmd_vel:pub;map:sub }, SECRET, algorithmHS256 ) return {token: token, expires_in: 7200}逻辑说明登录接口校验操作员口令通过后签发HS256 JWT。注意这里只用了最简的固定口令做演示生产环境建议对接自己的账号服务或至少换成 bcrypt 密文比对。exp设7200秒是为了一次巡检任务足够用但又不会让token长时间有效。app.post(/api/authz) def authorize(authorization: str Header(None, aliasAuthorization)): # Nginx的auth_request会转发原始Authorization头到这里 if not authorization or not authorization.startswith(Bearer ): raise HTTPException(status_code401, detailmissing token) token authorization.split( , 1)[1] try: payload jwt.decode(token, SECRET, algorithms[HS256]) return {x_auth_user: payload.get(sub, )} except jwt.PyJWTError: raise HTTPException(status_code401, detailtoken invalid or expired)逻辑说明/api/authz是被Nginx内部调用的鉴权接口成功时返回2xx即可Nginx不关心响应体内容失败时必须返回401。这里的x_auth_user会被Nginx捕获之后可以透传给上游rosbridge或其他Web服务。algorithms参数要显式指定HS256不要省略否则存在算法混淆攻击的风险。启动时用uvicorn token_auth:app --host 127.0.0.1 --port 8000跑起来记住这个鉴权服务只要监听本机就行不需要暴露到局域网。3.2 Nginx auth_request完整配置把ROS HTTP接口保护起来当鉴权服务起来之后Nginx配置可以直接套用下面这份。假设机器人内部HTTP服务跑在9090端口可以是rosbridge也可以是任何ROS Web界面# /etc/nginx/conf.d/robot_gateway.conf server { listen 443 ssl; ssl_certificate /etc/nginx/ssl/robot_fullchain.crt; ssl_certificate_key /etc/nginx/ssl/robot_key.key; # 内部鉴权入口只有Nginx自己可以访问 location /auth { internal; proxy_pass http://127.0.0.1:8000/api/authz; proxy_pass_request_body off; proxy_set_header Content-Length ; proxy_set_header Authorization $http_authorization; proxy_set_header X-Original-URI $request_uri; } location / { auth_request /auth; auth_request_set $auth_user $upstream_http_x_auth_user; proxy_pass http://127.0.0.1:9090; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header X-Auth-User $auth_user; } }逻辑说明location /auth是内部子请求地址返回200则进入后续转发。proxy_pass_request_body off;和Content-Length 一起使用避免把原始请求体再次传给鉴权服务既能省流量也避免因body过大导致鉴权接口超时。auth_request_set把鉴权接口返回的x_auth_user头放进变量再以X-Auth-User头传给上游这样rosbridge或Web服务能拿到身份信息。proxy_set_header Upgrade和Connection upgrade是WebSocket握手的关键缺少这两行浏览器连接会停在101切换协议阶段。3.3 过期时间、密钥轮换与上游超时这三组参数要一起调第一组是过期时间。我建议短期token给30分钟到2小时另配一个7天有效的refresh token专门用来换新token前端在检测到401后自动走刷新流程。这样用户不用频繁重新登录但泄露出短期token的破坏窗口被压到很小。refresh token存HttpOnly Cookie走单独的刷新接口不入localStorage。第二组是密钥轮换。JWT标准支持kid头最简单做法是在签发时把密钥版本写进载荷鉴权时根据版本选不同密钥。小项目也可以不做版本管理但至少要在配置里留一个切换开关让机器人在不重启业务服务的情况下能换密钥。第三组是鉴权上游超时。auth子请求一旦慢整个页面就会卡住。给/auth所在的location单独设置短超时location /auth { proxy_connect_timeout 1s; proxy_read_timeout 1s; proxy_send_timeout 1s; }逻辑说明三个超时分别限制连接鉴权服务、读取鉴权响应、发送鉴权请求的时间。1秒对本地FastAPI足够响应超过1秒便视为异常页面会快速失败而不是长时间白屏。如果线上偶尔出现502优先查鉴权服务本身而不是加超时时间。注意整个网关必须走HTTPS否则token在局域网里被中间人截获前面做的所有认证等于白搭。自签证书可以但要把根证书装到每台访问终端的信任库里不然浏览器会直接拦死。4. WebSocket的认证实时通道不能只做一次握手就算完HTTP接口的认证相对直白真正的坑集中在WebSocket这条路上。rosbridge通常是WebSocket长连接浏览器管理机器人指令基本都走这条通道。如果WebSocket没认证好前面Nginx挡得再严控制指令还是可以从别的路径透进去。4.1 浏览器不给自定义Headertoken怎么进入WebSocket握手WebSocket握手本质上是一次HTTP升级请求但它和普通HTTP请求有一个明显区别浏览器的new WebSocket(url)不允许设置自定义HeaderAuthorization头在构造函数里根本带不进去。这是大量项目翻车的起点——后端说“你带token啊”前端说“我带不进去”。常见的做法有两种把token放在URL query参数里或者放在Sec-WebSocket-Protocol子协议字段里。前一种实现简单适合绝大多数场景后一种符合“协议协商”的语义但需要rosbridge和自定义子协议配合复杂度更高。我最常用的是URL参数方案// ws_client.js —— 带token建立rosbridge连接 const token localStorage.getItem(robot_token); if (!token) { throw new Error(no token, please login first); } // 路径里的token会在Nginx层被提取出来拼成Authorization头 const ws new WebSocket( wss://robot.local/ws?token${encodeURIComponent(token)}, [rosbridge] ); ws.onopen () { console.log(rosbridge connected); };逻辑说明前端把token放到URL里绕开浏览器无法自定义Header的限制。用encodeURIComponent包裹是为防止token里有特殊字符。[rosbridge]是rosbridge要求的子协议保持这个值不变。注意这个方案的弊端是token会出现在Nginx access log和浏览器历史里所以token必须短时效最好不超过30分钟。4.2 在Nginx层把URL参数转成Authorization头含map配置前端把token放在URL query后Nginx侧需要用map指令把它转换成鉴权所用的Authorization头。这个map要放在nginx.conf的 http 块内、server块之外# 在nginx.conf的http块中定义注意要在server块之前 map $arg_token $auth_header { default $http_authorization; ~^(.)$ Bearer $1; }逻辑说明$arg_token是Nginx内置变量自动读取URL中的token参数。当URL带token时正则~^(.)$命中$auth_header就变成Bearer token当URL没带token时回落到$http_authorization也就是普通HTTP请求自带的Authorization头。这样一个变量同时兼容WebSocket和普通HTTP。然后在鉴权子请求里使用这个变量location /auth { internal; proxy_pass http://127.0.0.1:8000/api/authz; proxy_set_header Authorization $auth_header; } # WebSocket专用路由避免和普通POST请求混在一起 location /ws/ { auth_request /auth; auth_request_set $auth_user $upstream_http_x_auth_user; proxy_pass http://127.0.0.1:9090/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header X-Auth-User $auth_user; }逻辑说明/ws/路由单独处理WebSocket升级proxy_pass末尾带/会把/ws/前缀剥掉直接把请求转发给rosbridge的/路径这样rosbridge不用感知外部路由结构。auth_request在握手阶段执行一次鉴权通过后Nginx才把升级请求转发给上游。4.3 长连接不能只靠一次握手动态期限与心跳续期网关层认证的局限在这里体现得很明显WebSocket握手只发生一次连接建立后Nginx不会再对每条消息做鉴权。于是出现一个很尴尬的场景——token已经过期但连接还活着操作员还在通过这条连接发控制指令权限实际上悬空。我习惯在这层加一个“心跳续期”机制。前端每隔一段时间把token发给机器人上的会话服务服务端校验token的新鲜度发现过期就主动断开连接# ws_guard.py —— 对已建立的连接做周期性重新鉴权 import asyncio async def keepalive_guard(ws, token, auth_service, interval30): 每隔interval秒校验一次token失败则断开WebSocket while not ws.closed: await asyncio.sleep(interval) ok await auth_service.verify(token) if not ok: await ws.close(code4401, reasontoken expired) break逻辑说明interval30表示每30秒校验一次对机器人控制场景足够灵敏又不会产生太大网络开销。4401在WebSocket规范里属于自定义关闭码前端能据此弹出重新登录提示。注意不要在rosbridge的onmessage回调里做同步鉴权那会阻塞消息队列导致指令延迟重排。提示如果机器人对实时性要求极高比如需要200Hz以上的控制频率30秒的心跳校验也会挤占带宽。可以把校验频率降到60秒并在网关层增加总连接时长上限比如最长允许连接4小时到期强制重连。5. 避坑ROS Web认证中的五个典型问题与排查思路这一节记录的是我在实际部署中遇到的坑每条都按“现象 → 原因 → 解决”的顺序写排查时可以直接对照。5.1 reload后WebSocket握手从“顺滑”变“翻车”现象Nginx配置改完执行nginx -s reloadHTTP接口还正常但浏览器WebSocket一直握手失败Nginx错误日志里出现upstream prematurely closed connection或502。原因/auth子请求默认会转发原始请求体而WebSocket升级请求带有Upgrade头。鉴权服务拿到一个带Upgrade的POST请求后可能不知道如何响应或者因为Content-Length不匹配直接断开。问题不在业务服务而在auth子请求的转发参数。解决确认/auth里已经设置proxy_pass_request_body off;和proxy_set_header Content-Length ;。这两行必须同时存在缺一个都可能让鉴权接口读到一个空壳请求体。5.2 HTTP能过、WebSocket却401Authorization头根本没带过去现象浏览器访问Nginx保护的HTTP接口一切正常但同一个token建立WebSocket连接时始终报401看Nginx access log发现请求根本没到鉴权服务。原因前端new WebSocket()不带Authorization头而Nginxauth_request默认只转发Upgrade请求的原始headertoken放在URL query里的话原始header里根本没有Authorization鉴权服务自然判定未登录。解决用上一章4.2节的map方案把URL参数转成Authorization头。排查时可以先用命令行工具模拟一次带?token的握手确认Nginx是否产生了正确的子请求。5.3 CORS预检被auth_request误杀现象前端页面和机器人不在同一个域浏览器发出跨域请求时先发OPTIONS预检Nginx把预检请求也丢给鉴权服务鉴权服务不认识OPTIONS直接返回401前端报CORS错误。原因Nginx的auth_request对每个请求都会执行包括CORS预检。预检请求本身不带业务token被鉴权服务拒绝之后浏览器得不到正确的CORS响应头整个请求被中断。解决在受保护的location里对OPTIONS直接返回204不再触发auth_requestlocation / { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type; return 204; } auth_request /auth; proxy_pass http://127.0.0.1:9090; }逻辑说明if在location里只做return不会引入Nginx的if陷阱。提前放行OPTIONS是为了让浏览器能拿到允许跨域的响应头。Access-Control-Allow-Headers必须包含Authorization否则浏览器会认为自定义Header不被允许。5.4 token过期后连接还活着长连接不能只认握手现象操作员在机器人旁边操作了很久token已经过期但网页上的控制按钮还能用底盘还能响应指令。前端明明在很早之前就收到了401。原因握手鉴权只发生在连接建立瞬间之后所有消息都直接透传给rosbridgeNginx不再检查token。这个问题的危害比HTTP接口更大因为WebSocket是长连接控制通道可以一直开着。解决按4.3节的keepalive_guard方案增加周期性校验。如果不想动后端代码也可以在Nginx层面用map加近似的请求限流但最可靠的还是在业务层的消息代理里做校验。不要指望前端主动断开权限控制永远要在服务端执行。5.5 wss连不上、ws却没事证书链不完整才是黑匣子现象机器人同时开HTTP和HTTPS浏览器用ws://连接一切正常换成wss://就报证书错误或连接失败但Nginx证书配置明明看着没问题。原因wss://要求的是完整证书链。很多自签证书只生成了服务器证书没有把中间CA证书合进去浏览器不认识这个签发者直接拒绝连接。ws://不校验证书所以表现正常给人造成“配置没毛病”的错觉。解决检查机器人上的证书文件是否包含完整链最简单方式是数一下证书块数量grep -c BEGIN CERTIFICATE /etc/nginx/ssl/robot_fullchain.crt逻辑说明正常fullchain文件应包含2个以上certificate块服务器证书一个中间CA一个或两个。如果输出是1说明只有服务器证书需要把中间CA证书追加到同一个文件里。追加之后记得nginx -t nginx -s reload。6. 从“能进”到“能做什么”ROS话题级授权的一种轻量实现网关层解决的是“你是谁”但真正决定一个操作员能不能按“启动”按钮的是“你能做什么”。这个话题级授权我习惯在rosbridge前面再放一个轻量消息代理对JSON-RPC做逐条topic过滤而不是把所有控制逻辑堆在Nginx里。6.1 在JSON-RPC消息进ROS之前做话题过滤rosbridge收到的消息长这样{op:publish,topic:/cmd_vel,msg:{...}}。基于Token里预先声明的scope可以在消息进入ROS之前拒绝掉越权指令# topic_guard.py —— 在JSON-RPC消息进ROS前做话题级过滤 import json SCOPE_POLICY { cmd_vel: {pub: True, sub: False}, map: {pub: False, sub: True}, battery: {pub: False, sub: True}, } def guard_message(token_scope: dict, raw_msg: str): 解析rosbridge JSON-RPC校验操作是否匹配scope返回None表示拒绝 msg json.loads(raw_msg) op msg.get(op) topic msg.get(topic, ).lstrip(/) if op in (publish, subscribe): allowed token_scope.get(topic, {}) if not allowed.get(pub if op publish else sub): return None if op call_service: service msg.get(service, ) if service not in token_scope.get(services, []): return None return raw_msg逻辑说明这个代理不对消息做解密或透传只做一个动作解析JSON判断op与topic是否在scope中允许不允许就丢弃。token_scope由前面的JWT载荷解析出来可以在握手时初始化。这样即使某个操作员拿到了合法连接也只能 publish 到被授权的topic无法乱动别的节点。6.2 把权限绑定到token而不是IP事后才能定位到人最后一点习惯不要把权限绑定到IP。局域网里IP冲突、DHCP翻车、甚至隔壁工位临时借用一台电脑都会让“根据IP判断身份”失效。把权限绑到token上每个操作员拿到的token是独立的一旦发现机器异常动作直接查网关日志里的sub字段比查IP可靠得多。我后来把这一整套链路整理成固定的排查步骤先查Nginx日志里有没有401再查鉴权服务日志里有没有decode失败最后查消息代理日志里有没有被topic_guard丢弃的记录。这半年在token过期上被反复折磨过好几次最后发现最值得投入的还是把整个认证链路做成闭环——网关负责挡陌生人消息代理负责管熟人两者缺一不可。希望这一篇能帮到你少走几步弯路。本文还有配套的精品资源点击获取