Linux Nginx 后端通过请求头判断移动端还是PC端怎么配置

发布时间:2026/10/4 11:52:40
Linux Nginx 后端通过请求头判断移动端还是PC端怎么配置
前言典型需求是这样同一套后端程序PC 用户访问www.example.com时返回桌面版页面手机用户访问同一个域名时返回移动版页面或直接跳转到m.example.com。后端不想自己解析User-Agent字符串又长又乱还容易被改希望 Nginx 在前面把「这是手机还是电脑」判断好通过一个自定义请求头告诉后端后端只读这个头做分支。本文就写这个Nginx 侧用map从请求头里提取设备类型再通过proxy_set_header传给后端。所有片段基于 RHEL 9 / nginx 1.24配置在/etc/nginx/nginx.conf站点文件放/etc/nginx/conf.d/*.confDebian/Ubuntu 上路径与写法一致站点目录换成/etc/nginx/sites-enabled/即可。有一件事必须在开头说清楚基于User-Agent判定的设备识别本质上是一种启发式猜测不是可靠信号。客户端可以任意伪造User-Agent各家爬虫、微信内置浏览器、iPadOS 上「请求桌面网站」开关、各种安卓定制浏览器都会让规则失效。所以这套机制的正确用法是「让绝大多数真实用户看到合适的页面」不能拿它当权限或安全边界也不要指望命中率是 100%。一、能用来判断的请求头有哪些请求头Nginx 变量可用性说明User-Agent$http_user_agent所有 HTTP 客户端都会发送最通用的判断依据本文主用这个Sec-CH-UA-Mobile$http_sec_ch_ua_mobile仅 HTTPS且浏览器支持 Client Hints值是?0或?1比解析 UA 字符串干净得多Sec-CH-UA-Platform$http_sec_ch_ua_platform同上值为Android、iOS、Windows等注意带引号自定义头如X-Device$http_x_device取决于谁注入常见于 CDN、App WebView 或自家客户端主动上报X-Forwarded-Proto$http_x_forwarded_proto由前置层注入判断真实协议用与设备判断无关但常一起出现两点提醒Nginx 变量名规则$http_前缀加上请求头名字母小写、连字符换成下划线。所以X-Device对应$http_x_deviceSec-CH-UA-Mobile对应$http_sec_ch_ua_mobile。请求头不存在时变量为空字符串不会报错。Client Hints 需要协商Chromium 系浏览器在 HTTPS 下才会发送Sec-CH-UA-MobileFirefox 和 Safari 目前基本不发。为了让兼容性最好实践中还是以User-Agent为主、Client Hints 为辅。二、用 map 生成设备类型变量判断逻辑要写在http上下文里的map指令而不是在server/location里写if。原因是map的求值结果可以缓存到变量全配置任意位置复用而if是请求处理阶段的即时判断写在location里容易与proxy_pass、try_files、add_header互相干扰社区里那句「if is evil」说的就是这个问题。# /etc/nginx/conf.d/mobile.conf map $http_user_agent $device_type { default pc; # 移动端按覆盖面从宽到窄排注意正则大小写不敏感用 ~* ~*ipod|iphone|android.*mobile|windows phone|blackberry|opera mini|iemobile mobile; ~*ipad|android|kindle|silk|playbook tablet; }map的匹配规则必须记牢否则会出现「明明写了规则却全落到 default」的问题先做字符串精确匹配不带~前缀的写法。再做正则匹配~区分大小写、~*不区分大小写多个正则按在配置里出现的顺序依次尝试第一个命中的生效。都不命中用default。没有default且都不命中时结果为空字符串。所以上面的顺序是经过设计的先判手机android.*mobile只在手机版安卓 UA 里出现再判平板剩下带android的是平板最后兜底pc。如果要按域名做映射map支持hostnames选项例如map $http_host $site_code { hostnames; default default; example.com main; *.example.com sub; }正则里含有{、}、;时要用双引号把模式括起来否则 Nginx 会把它当成块分隔符导致nginx -t报语法错误。三、把结果传给后端Nginx 只负责判断真正的分流交给后端配置最简单——加一个请求头server { listen 80; server_name www.example.com; location / { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 设备类型后端读这个头做分支 proxy_set_header X-Device-Type $device_type; # 同时把原始 UA 传下去方便后端做补充判断或写日志 proxy_set_header User-Agent $http_user_agent; } }后端侧以 PHP 为例读的就是$_SERVER[HTTP_X_DEVICE_TYPE]Java 侧是request.getHeader(X-Device-Type)。注意请求头的名字到后端语言里的键名转换规则别写成X_DEVICE_TYPE。这里有个必须注意的缓存陷阱。如果 Nginx 开了proxy_cache缓存键默认只包含$scheme$request_method$host$request_uri不含X-Device-Type。结果是第一个访问者的版本被缓存下来后面所有设备都拿到它——手机上刷出桌面版页面或者反过来。修法是显式把设备类型加进缓存键proxy_cache_key $scheme$request_method$host$request_uri$device_type; # 如果上游还有一层缓存告诉它响应随请求头变化 add_header Vary User-Agent, X-Device-Type always;反向代理层同样要设proxy_set_header把设备类型一并转发否则后端读到的永远是空值程序里if mobile的分支一次都不会走。这是最容易漏的一环。四、实战一套可直接粘贴的配置与验证以下基于 RHEL 9 / nginx 1.24把map放在http块内、server放在conf.d里。Debian/Ubuntu 上请把文件放到/etc/nginx/sites-enabled/mobile.confuser用www-data其余完全相同。# /etc/nginx/conf.d/mobile.conf # map 必须在 http 上下文不能写在 server 里 map $http_user_agent $device_type { default pc; ~*ipod|iphone|windows phone|blackberry|opera mini|iemobile|android.*mobile mobile; ~*ipad|android|kindle|silk|playbook tablet; } # 用设备类型选静态根目录 map $device_type $site_root { default /var/www/example/pc; mobile /var/www/example/mobile; tablet /var/www/example/tablet; } server { listen 80; server_name www.example.com; access_log /var/log/nginx/example.access.log main; error_log /var/log/nginx/example.error.log warn; # 便于排查把判断结果回显在响应头里 add_header X-Device-Type $device_type always; # 静态站点按设备切换根目录 root $site_root; index index.html; location / { try_files $uri $uri/ /index.html?$query_string; } # 动态接口把设备类型透给后端 location /api/ { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Device-Type $device_type; } }root的值里可以含变量但变量中不允许出现$document_root和$realpath_root会形成循环引用。上游后端需要在http块里先定义upstream app_backend { server 10.0.0.11:8080 max_fails3 fail_timeout10s; keepalive 32; }加载并验证sudo nginx -t sudo nginx -s reload用不同User-Agent请求看回显的头-I发 HEAD比 GET 快# 模拟 iPhone curl -sI -A Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1 \ -H Host: www.example.com http://127.0.0.1/ | grep -i x-device-type # 模拟安卓手机 curl -sI -A Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36 \ -H Host: www.example.com http://127.0.0.1/ | grep -i x-device-type # 模拟桌面 Chrome curl -sI -A Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 \ -H Host: www.example.com http://127.0.0.1/ | grep -i x-device-type # 不带 UA很多脚本和健康检查就是这样 curl -sI -H Host: www.example.com http://127.0.0.1/ | grep -i x-device-type预期依次是mobile、mobile、pc、pc。如果四条全是pc八成是map写在了server块里没生效或者正则里多了该转义的字符。要确认后端真的收到了头最直接的方法是让后端把X-Device-Type原样回显或者临时在 Nginx 上打一份带变量的日志log_format device_debug $remote_addr $http_user_agent device$device_type status$status;排查请求头类问题有一个通用杀手锏如果怀疑是 Nginx 加的头发错了可以把它转发到一个已知会回显全部请求头的地方比如内网的调试服务用curl -v对比「客户端发出去的」和「Nginx 转出去的」两套头。绝大多数「后端读不到头」的问题都是漏了proxy_set_header或写错了头的名字。常见坑点❌ 把map写进server或location块nginx -t报map directive is not allowed here✅map只能出现在http上下文通常单独放一个conf.d/xxx.conf由nginx.conf里的include /etc/nginx/conf.d/*.conf;带进来❌ 用if ($http_user_agent ~* mobile) { set $device_type mobile; }逐步拼变量 ✅ 统一用map一次算好map只在变量被引用时求值不增加常驻开销而if与proxy_pass、add_header混用会有微妙的作用域问题❌ 正则写成~* (iPhone|Android)以为能匹配实际 UA 里还有一堆变体漏判严重 ✅ 规则要按真实 UA 特征写手机安卓一定含Mobile平板安卓不含所以用android.*mobile先判手机、再用android兜平板❌ 开了proxy_cache却没把$device_type加进proxy_cache_key✅proxy_cache_key $scheme$request_method$host$request_uri$device_type;并用Vary通知上游缓存❌ 只在前端 Nginx 上判断后端那一层反代没有proxy_set_header X-Device-Type $device_type;✅ 每一跳都要显式转发这个头中间任何一跳漏掉后端读到的都是空字符串❌ 用Sec-CH-UA-Mobile作为唯一判断依据结果 Firefox 和 Safari 用户全被当成 PC ✅ 以User-Agent为主判据Client Hints 只作为覆盖增强并注意它仅在 HTTPS 下才会被浏览器发送❌ 把设备判断结果当成可信身份用它决定「能否看到某个页面」或做风控 ✅ 所有请求头都由客户端控制curl -A iPhone一秒就能伪装设备类型只能用于体验优化权限判断必须走服务端会话❌ 用add_header在if块里回显$device_type发现头没加上 ✅add_header放在server或location层级同时注意父级add_header会被子级块里出现的add_header整体覆盖不继承排查时留意这一点总结目标做法判断设备类型map $http_user_agent $device_type { ... }写在http上下文匹配顺序先精确字符串、再按配置顺序尝试正则、最后default正则含{};要加双引号传给后端proxy_set_header X-Device-Type $device_type;每一跳反代都要转发Nginx 自己分流静态资源map $device_type $site_root { ... }再root $site_root;带缓存时proxy_cache_key必须包含$device_type并加Vary验证add_header X-Device-Type $device_type always;curl -I -A ...可靠性认准这是启发式判断只用于体验优化不做权限依据配置本身只有三步map算类型、proxy_set_header传下去、缓存键带上它。真正决定成败的是细节——map的上下文和匹配顺序、每一跳都要转发、缓存不能串版本。把这三处对齐Nginx 就能稳定地告诉后端「来的是手机还是电脑」。