引子
在使用私有镜像仓库时突然出现了认证问题,跟着 AI 来回改,在此记录一下改的过程。
首先回顾一下之前的那篇构建私有镜像仓库的文章,我们建立了两个容器,registry用来存放镜像,registry-UI用来给前端显示。
同时在服务器上使用 nginx 进行了反向代理,对浏览器传来的 https 请求进行处理
- 两个80 端口用作
Let’s Encrypt的 HTTP 验证 - 两个443 端口:一个用作 Registry,转发到本地
:5000端口;一个用作 UI 界面,转发到本地的:8080端口,这两个本地端口其实都是容器服务所暴露的端口。
错误过程
CORS
一开始访问前端 UI 界面,显示了红色错误,显示Access-Control-Allow-Origin header must be set to https://registryui.bfsmlt.top
这是一个 CORS 问题,为什么呢?因为浏览器前端页面https://registryui.bfsmit.top,会使用 js 向镜像仓库后端发送请求。
而镜像后端在https://jimlt.bfsmlt.top上,二者不同源。
关于Cross-Origin Requests,简单描述一下这个概念:
- 协议,端口,域名三者有一个不同就不是同源
- 浏览器默认采用同源策略,防止不同源的网站读取当前网站的敏感信息
- 在 CORS 下允许服务器声明哪些源可以访问其资源
令我惊讶的是之前使用中一直没有发现问题,而且我在之前那篇文章中在启动 registry 容器时也已经指定了REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin等相关参数,按理来说应该不会出现 CORS 问题。
说到这里我觉得有必要梳理一下整个流程
- 浏览器访问
https://registryui.bfsmlt.top - 服务器中的 nginx 将这个请求转发给 ui 容器,即
http://localhost:8080 - 容器返回给浏览器所需的
HTML,CSS,JS;浏览器借此组成界面 - 浏览器中
JS根据容器设定的参数会去访问https://jimlt.bfsmlt.top/v2/_catalog来获取镜像列表 - 而此时浏览器发现当前网页是在
https://registryui.bfsmlt.top加载的,与要访问的https://jimlt.bfsmlt.top/v2/_catalog不同源;发生 CORS 问题 - 浏览器会发送一个
OPTIONS预检请求,询问jimlt.bfsmlt.top是否同意这次跨域请求
所以关键就在这里了,可能我们的容器运行时参数不够,得去修改 nginx,让jimlt.bfsmlt.top允许这次跨域请求。
为 jimlt.bfsmlt.top 的 server 区域添加如下的预检请求,本质就是增加头部信息。
# 处理浏览器的预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://registryui.bfsmlt.top' always;
add_header 'Access-Control-Allow-Methods' 'HEAD, GET, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# 为实际的 API 也加上标头
add_header 'Access-Control-Allow-Origin' 'https://registryui.bfsmlt.top' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
502 Bad Gateway
这样修改之后又出现了新问题<html><head><title>502 Bad Gateway</title></head><body><center><h1>502 Bad Gateway</h1></center><hr><center>nginx</center></body></html>
这意味着 Nginx 接收到了浏览器的请求,但是将请求转发给 registry 容器时失败了。
我试着直接访问这个容器curl -I http://127.0.0.1:5000/v2/,显示无法连接。
于是重启 docker 服务,再次尝试时不再是 502 问题了。终端出现认证问题是 401,因为我们之前设置过密码;
但是,令人不解的是,浏览器访问页面时又出现了 CORS 问题。
试着去掉缓存,去掉 cookie,不起作用。问题在于,此时浏览器应该给我出现填写用户名和密码的表单才对。
更神奇的来了,我直接访问https://jimlt.bfsmlt.top/v2/_catalog成功了,可以看到镜像列表。
所以我觉得还是从浏览器访问registry容器的这个过程出现了问题。
UI升级
反思一下,当时我参考的自建镜像仓库的文章来源于 2020年的一篇博客,这个 registryUI 在 2021年进行了一次较大的改动。
所以来到了改动页面,看看我们需要改什么参数。
可以发现最大的变化在于以下几点:
REGISTRY_URL改为了NGINX_PROXY_PASS_URLURL改为了REGISTRY_URL- 从旧版本迁移时,
SINGEL_REGISTRY = true
其中NGINX_PROXY_PASS_URL就是为了处理可能出现的 CORS 问题,这个字段表示 UI 容器内部的反向代理,至于到底发生了什么,见下方的最终修改。
最终修改
到了这里,我感觉到应该是 UI 的版本更替问题,因为在之前使用过程中,好像出现过更新的提示。
所以我决定将 registry 仓库和 UI 全部进行重建,并将他们放在同一个 dockerCompose 里面进行操作,这样可以充分利用 compose 的便利性。
- 由于卷挂载的缘故,重建过程并不会影响之前已经存在的镜像和 TLS 信息
dokcer-compose 文件内容如下:
version: '3.8'
services:
# 後端 Registry 服務
registry:
image: registry:2
container_name: registry
restart: always
ports:
- "5000:5000"
volumes:
- /var/lib/registry:/var/lib/registry
- /etc/docker/registry/auth:/auth
# 如果您的憑證也想在這裡管理,也可以加上,但目前您的 Nginx 在使用,保持現狀即可。
# - /etc/docker/registry/certs:/certs
environment:
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
networks:
- registry-net
# 前端 UI 服務 (使用我們最終確定的 v2 版本配置)
registry-ui:
image: joxit/docker-registry-ui:main
container_name: registry-ui
restart: always
ports:
- "8080:80"
environment:
SINGLE_REGISTRY: "true"
NGINX_PROXY_PASS_URL: "http://registry:5000"
REGISTRY_TITLE: "My Private Registry"
DELETE_IMAGES: "true"
depends_on:
- registry
networks:
- registry-net
networks:
registry-net:
driver: bridge
可以看到在 UI 服务中这两个参数SINGLE_REGISTRY: "true",NGINX_PROXY_PASS_URL: "http://registry:5000"
- JS的请求此时不会直接打到 registry 容器,而是访问 UI 容器
GET https://registryui.bfsmlt.top/v2/_catalog - UI 容器的内部 nginx 会将发来的请求反向代理到
http://registry:5000 - 而 registry 正是镜像容器的服务名称,通过 bridge 网络结构直接发送了过来。
- 最终后端容器接收请求,处理后再通过原路返回给前端。
注意,这个过程就不会出现 CORS 问题,因为SINGLE_REGISTRY: "true"的缘故,浏览器访问的还是同源下的路径,到了我们的服务器 Docker 环境中才转换了访问。
- 这里用到了 compose 中的 bridge网络结构,利用了其内建 DNS 的特性——将服务名作为主机名来访问容器。
最后成功在浏览器进行访问,获取了镜像列表。
Github issues
一开始遇到问题询问 AI 我觉得无可厚非,但是在尝试过程中发现可能是 UI 出现问题,早该去对应的仓库中找找有没有类似的问题可以参考。
找到了最近的相关 issue,可以发现就是版本更新带来的参数定义重置问题。
并且在 README 中,作者也提到了这个问题,他的建议是
I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/ or use NGINX_PROXY_PASS_URL or configure a nginx/apache/haproxy in front of your registry that returns 200 on each OPTIONS requests.
- 不要跨域
- 设置
NGINX_PROXY_PASS_URL参数 - 设置
nginx/apache/haproxy
同时他也提到了如果不进行设置会导致 CORS 的最终原因:
This is caused by a bug in docker registry, it returns 401 status requests on preflight requests, this breaks W3C preflight-request specification.
即 registry 在收到预检请求 Options 后不会进行预检,还是会强调身份认证,所以预检失败,自然触发 CORS.
总结
有趣的一次经历,巩固了 nginx 和 CORS 的相关知识。
这里是LTX,感谢您阅读这篇博客,人生海海,和自己对话,像只蝴蝶纵横四海。