.htaccess 如何避免对 REST API 请求应用重写规则
结论
Android 端的内部重写需要同时排除以下两种情况:
- 客户端最初请求的是
/api或/api/rest/ - 当前内部请求已经进入
/pwa
API 判断建议使用 %{THE_REQUEST}。REST 规则将请求内部重写为 index.php 后,Apache 可能再次处理 .htaccess,使用该变量仍能识别原始 API 请求。是否已进入 /pwa 则应通过 %{REQUEST_URI} 判断,以免重复添加 /pwa。
推荐规则如下:
# Android 普通页面内部映射到 /pwa
RewriteCond %{HTTP_USER_AGENT} Android [NC]
RewriteCond %{THE_REQUEST} !\s/+api(?:/|[?\s]) [NC]
RewriteCond %{REQUEST_URI} !^/pwa(?:/|$) [NC]
RewriteRule ^(.*)$ pwa/$1 [END]
[END] 适用于 Apache 2.4,它会终止当前请求后续的所有按目录重写处理。由于规则没有使用 R 标志,浏览器地址不会改变。
原规则为什么会出问题
原规则如下:
RewriteCond %{HTTP_USER_AGENT} Android [NC]
RewriteCond %{THE_REQUEST} !\s/+(pwa|api) [NC]
RewriteRule ^ pwa%{REQUEST_URI} [L]
这里有两个问题。
API 请求在内部重写后可能再次匹配移动端规则
REST API 规则会把:
/api/rest/categories
内部重写为:
/index.php?route=feed/rest_api/categories
内部重写完成后,Apache 可能再次处理 .htaccess。如果只检查当前的 %{REQUEST_URI},第二轮看到的可能已经是 /index.php,无法再判断原请求是否来自 API。Android 规则便可能错误地把请求重写到 /pwa/index.php。
%{THE_REQUEST} 保存客户端发送的原始 HTTP 请求行,例如:
GET /api/rest/categories HTTP/1.1
即使请求随后被内部重写为 index.php,%{THE_REQUEST} 仍会保留原始的 /api/rest/categories,因此可以在每一轮重写中排除 API 请求。
不能只用 %{THE_REQUEST} 防止重复添加 /pwa
假设客户端请求:
/products
第一次内部重写后,请求变为:
/pwa/products
但 %{THE_REQUEST} 仍是原始内容:
GET /products HTTP/1.1
其中并没有 /pwa。如果 .htaccess 再次执行,只检查 %{THE_REQUEST} 无法阻止规则第二次添加 /pwa。
所以还需要检查当前 URI:
RewriteCond %{REQUEST_URI} !^/pwa(?:/|$) [NC]
推荐的规则顺序
.htaccess 建议按以下顺序组织:
- 域名和 HTTPS 跳转
- OpenCart 固定资源及认证头规则
- 所有明确的 REST API 规则
- Android
/pwa内部重写 - OpenCart 通用 SEO URL 规则
结构示例如下:
RewriteEngine On
RewriteBase /
# 1. 域名、HTTPS 等外部跳转应先完成
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# 2. OpenCart 固定规则
RewriteRule ^sitemap\.xml$ index.php?route=extension/feed/google_sitemap [L]
RewriteRule ^googlebase\.xml$ index.php?route=extension/feed/google_base [L]
RewriteRule ^system/storage/(.*)$ index.php?route=error/not_found [L]
# 3. 所有 REST API 规则放在通用 SEO 规则之前
RewriteRule ^api/rest/oauth2/token/?([a-zA-Z0-9_]+)$ index.php?route=feed/rest_api/gettoken&grant_type=$1 [L,QSA]
RewriteRule ^api/rest/oauth2/token/?$ index.php?route=feed/rest_api/gettoken [L,QSA]
RewriteRule ^api/rest/categories/?$ index.php?route=feed/rest_api/categories [L,QSA]
RewriteRule ^api/rest/user/([0-9]+)/?$ index.php?route=feed/rest_api/user&id=$1 [L,QSA]
# 这里继续保留其余 ^api/rest/ 规则
# 4. Android 普通页面内部加载 /pwa
RewriteCond %{HTTP_USER_AGENT} Android [NC]
RewriteCond %{THE_REQUEST} !\s/+api(?:/|[?\s]) [NC]
RewriteCond %{REQUEST_URI} !^/pwa(?:/|$) [NC]
RewriteRule ^(.*)$ pwa/$1 [END]
# 5. 最后才是 OpenCart 通用 SEO URL 规则
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !\.(?:ico|gif|jpe?g|png|js|css)$ [NC]
RewriteRule ^([^?]*)$ index.php?_route_=$1 [L,QSA]
REST API 规则必须放在通用 SEO URL 规则之前,否则 /api/rest/categories 之类的请求可能先被通用规则接管。
服务器不支持 [END] 时
[END] 从 Apache 2.4 开始可用。服务器版本较旧或主机不允许使用该标志时,可以换成 [L]:
RewriteCond %{HTTP_USER_AGENT} Android [NC]
RewriteCond %{THE_REQUEST} !\s/+api(?:/|[?\s]) [NC]
RewriteCond %{REQUEST_URI} !^/pwa(?:/|$) [NC]
RewriteRule ^(.*)$ pwa/$1 [L]
这种情况下,排除 %{REQUEST_URI} 中的 /pwa 更为重要。[L] 只会结束当前一轮规则处理,内部重写后 .htaccess 仍可能再次执行。
只排除 /api/rest/ 的写法
如果 /api 下还有普通页面,不应排除整个 /api,可以把条件收窄为:
RewriteCond %{THE_REQUEST} !\s/+api/rest(?:/|[?\s]) [NC]
完整规则如下:
RewriteCond %{HTTP_USER_AGENT} Android [NC]
RewriteCond %{THE_REQUEST} !\s/+api/rest(?:/|[?\s]) [NC]
RewriteCond %{REQUEST_URI} !^/pwa(?:/|$) [NC]
RewriteRule ^(.*)$ pwa/$1 [END]
如果所有 /api/... 路径都是后端接口,排除整个 /api 会更稳妥。
注意事项
- 内部重写规则不要添加
R、R=301或R=302,否则浏览器地址会显示/pwa/...。 .htaccess中,RewriteRule匹配的路径通常不以/开头,因此应写成^api/rest/、^pwa/,而不是^/api/rest/。%{REQUEST_URI}通常以/开头,对应条件应写成^/api或^/pwa。- 内部重写默认保留查询字符串。需要合并原查询参数和替换目标中的参数时,可以显式使用
[QSA]。 - User-Agent 可以被修改或伪造。如果移动站点的选择会影响认证、权限或其他安全逻辑,就不应只依赖
Android判断。它只适合用作展示层的路由条件。 - 示例中的
example.com.com看起来像域名笔误。如果实际请求确实使用该域名,还需确认虚拟主机、TLS 证书和跨域配置是否与目标域名一致。
备注:内容仅供参考。
