引子

在使用私有镜像仓库时突然出现了认证问题,跟着 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.topserver 区域添加如下的预检请求,本质就是增加头部信息。

        # 处理浏览器的预检请求
        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_URL
  • URL 改为了 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,感谢您阅读这篇博客,人生海海,和自己对话,像只蝴蝶纵横四海。