Nexus 私服部署 + HTTPS + 看门鹅 SSO 对接完整指南(Nexus 3.77 实战)
Nexus 私服安装部署完整文档(Nexus 3.77 + HTTPS + 看门鹅 SSO)
2026-09-18 终版 · 实战验证通过 · 适用:Debian VM + Docker + OpenVPS 环境
本文档覆盖从零安装 → HTTPS → SSO 全流程,按顺序执行即可复现整套部署。
架构总览(最终形态)
浏览器/Maven ──▶ maven.xzsoft.com(DNS → 线路机公网 IP)
├─ 面板规则 http(80) → VM:80 nginx(ACME 证书验证 + 301)
└─ 面板规则 https(443,SNI直通) → VM:443 nginx(Let's Encrypt 证书终结)
├─ /repository/** ─▶ 直通 Nexus(匿名拉包 / Basic 发布,不走 SSO)
├─ /service/**(带凭据) ─▶ 直通 Nexus(管理 API / API Token)
├─ /service/rapture/session(DELETE) ─▶ 拦截:清双 cookie(Sign out)
├─ /logout ─▶ 清本站 cookie → 看门鹅中心登出 → 登录页
├─ /oauth2/** ─▶ oauth2-proxy(OIDC 登录/回调)
└─ 其余(浏览器) ─▶ oauth2-proxy ─▶ Nexus 容器
未登录 302 → auth.xzsoft.com 授权(每次带 force=要求重新认证)
已登录注入 X-Forwarded-* → 插件 OAuth2ProxyRealm 认证 + 自动建用户选型依据:Nexus OSS 无官方 OIDC/SAML(Pro 付费功能);社区插件 nexus-oauth2-proxy-plugin
是 OSS 唯一接近原生体验的 SSO 方案,但 3.78 起官方封禁自定义插件(Karaf→Spring Boot 迁移,
无解)——3.77.2 是最后可用版本,禁止升级到 3.78+。
第一部分:安装 Nexus
1.1 前置(OpenVPS 面板开 VM)
- 参考《OpenVPS虚拟机部署服务手册.md》:面板 API 创建 VM(debian-13 镜像、注入公钥、记下
一次性 root 密码);octet 决定端口段(如 87 → 8700-8799) - SSH:
ssh -J root@<宿主机IP> root@172.16.0.<octet>;同 IP 重建后先ssh-keygen -R清旧 host key
1.2 Docker
⚠️ 新 VM 的 cloud-init 初始化会占用 apt 锁几分钟,先等锁释放再装:
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 5; done
apt-get update -qq && apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq && apt-get install -y docker-ce docker-ce-cli containerd.io1.3 Nexus 容器(⚠️ 三个关键点:属主 / 监听 / 版本)
mkdir -p /data/nexus/deploy
chown -R 200:200 /data/nexus # ⚠️ Nexus 容器进程 UID=200,root 属主会启动失败
docker network create nxnet
docker run -d --name nexus --restart always --network nxnet \
-p 127.0.0.1:8701:8081 \
-v /data/nexus:/nexus-data \
sonatype/nexus3:3.77.2 # ⚠️ 必须钉死 3.77.2,禁止 latest(3.78+ 插件失效)
# 等就绪(约 1-2 分钟)
until [ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8701/service/rest/v1/status)" = "200" ]; do sleep 10; done
cat /data/nexus/admin.password # 初始密码,仅此一次-p 127.0.0.1:8701:Nexus 绝不直接暴露公网(header 认证模式下外部可伪造 X-Forwarded-*
冒充任意用户;唯一入口必须是 nginx)。
1.4 初始化(REST API,全脚本化)
NA="admin:<自定管理密码>"; NX=http://127.0.0.1:8701/service/rest/v1
# ① 改 admin 密码
curl -s -u "admin:<初始密码>" -X PUT "$NX/security/users/admin/change-password" \
-H 'Content-Type: text/plain' -d "<自定管理密码>"
# ② ⚠️ 接受 EULA(3.71+ 强制;不做则全站 403,连 admin 都写不进!disclaimer 必须与 GET 响应逐字一致)
curl -s -u $NA "$NX/system/eula" -o /tmp/e.json
curl -s -u $NA -X POST "$NX/system/eula" -H 'Content-Type: application/json' \
-d "$(python3 -c 'import json;d=json.load(open("/tmp/e.json"));print(json.dumps({"accepted":True,"disclaimer":d["disclaimer"]}))')"
# ③ 启用匿名访问(mvn 拉包免认证)
curl -s -u $NA -X PUT "$NX/security/anonymous" -H 'Content-Type: application/json' \
-d '{"enabled":true,"userId":"anonymous","realmName":"NexusAuthorizingRealm"}'
# ④ 仓库:⚠️ Nexus 3 新装**默认已预置** maven-central(proxy)/maven-releases/snapshots(maven-public group)
# 四件套,直接可用!以下创建命令仅在需要自定义时执行(对默认仓库名执行会报 "Duplicate key" 400)。
# 可选自定义示例(writePolicy ⚠️ 3.77 枚举是 ALLOW_ONCE,不是 3.96 的 DISABLE_ONCE):
if false; then
curl -s -u $NA -X POST "$NX/repositories/maven/proxy" -H 'Content-Type: application/json' -d '{
"name":"maven-central","online":true,"storage":{"blobStoreName":"default","strictContentTypeValidation":true},
"proxy":{"remoteUrl":"https://repo1.maven.org/maven2/","contentMaxAge":-1,"metadataMaxAge":1440},
"negativeCache":{"enabled":true,"timeToLive":1440},"httpClient":{"blocked":false,"autoBlock":true},
"maven":{"versionPolicy":"MIXED","layoutPolicy":"PERMISSIVE"}}'
curl -s -u $NA -X POST "$NX/repositories/maven/hosted" -H 'Content-Type: application/json' -d '{
"name":"maven-releases","online":true,"storage":{"blobStoreName":"default","strictContentTypeValidation":true,"writePolicy":"ALLOW_ONCE"},
"maven":{"versionPolicy":"RELEASE","layoutPolicy":"STRICT"}}'
curl -s -u $NA -X POST "$NX/repositories/maven/hosted" -H 'Content-Type: application/json' -d '{
"name":"maven-snapshots","online":true,"storage":{"blobStoreName":"default","strictContentTypeValidation":true,"writePolicy":"ALLOW"},
"maven":{"versionPolicy":"SNAPSHOT","layoutPolicy":"STRICT"}}'
curl -s -u $NA -X POST "$NX/repositories/maven/group" -H 'Content-Type: application/json' -d '{
"name":"maven-public","online":true,"storage":{"blobStoreName":"default","strictContentTypeValidation":true},
"group":{"memberNames":["maven-releases","maven-snapshots","maven-central"]},
"maven":{"versionPolicy":"MIXED","layoutPolicy":"PERMISSIVE"}}'
fi
# 验证四件套存在:
curl -s -u $NA "$NX/repositories" | grep -oE '"name" : "maven-[a-z]+"' | sort -u
# ⑤ CI 发布账号
curl -s -u $NA -X POST "$NX/security/users" -H 'Content-Type: application/json' \
-d '{"userId":"deploy","firstName":"CI","lastName":"Deploy","emailAddress":"deploy@example.com",
"password":"<强随机>","status":"active","roles":["nx-admin"]}'Maven 客户端(settings.xml):
<mirror><id>nexus</id><mirrorOf>*</mirrorOf>
<url>https://maven.xzsoft.com/repository/maven-public/</url></mirror>1.5 可选:关闭首页公告(Outreach)
新装实例首页会显示 Sonatype 推送的广告性内容(如 "Known Issue for Azure Blob Store Users",
与本地部署无关)和欢迎页推广卡片——由 Outreach: Management capability 下发,禁用即消失:
# ① 找到 Outreach capability 的 id
curl -s -u $NA -X POST http://127.0.0.1:8701/service/extdirect -H 'Content-Type: application/json' \
-d '{"action":"capability_Capability","method":"read","type":"rpc","tid":1,"data":[]}' \
| grep -oE '"id":"[^"]*","typeId":"OutreachManagementCapability"' | grep -oE '"id":"[^"]*"'
# ② 用该 id 禁用(下面命令把 <id> 替换为上一步结果)
curl -s -u $NA -X POST http://127.0.0.1:8701/service/extdirect -H 'Content-Type: application/json' \
-d '{"action":"capability_Capability","method":"disable","type":"rpc","tid":2,"data":["<id>"]}'纯展示层操作,不影响任何功能;也可在 UI 的 System → Capabilities 里对 "Outreach: Management" 点禁用。
第二部分:HTTPS(nginx + Let's Encrypt)
2.1 DNS 与转发规则(先建 http 规则才能签证书!)
- DNS:
maven.xzsoft.comA 记录 → 线路机公网 IP(不是宿主机) - 面板 API 建 http 规则(ACME 验证流量的入口):
curl -sk -X POST https://<面板>/panel/api/proxy -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"rule_type":"http","server_name":"maven.xzsoft.com","backend_host":"172.16.0.<octet>","backend_port":80,"machine_id":<id>,"enabled":true}'2.2 VM 内 nginx + 签证书
apt-get install -y nginx certbot
mkdir -p /var/www/html
cat > /etc/nginx/sites-available/maven <<'EOF'
server {
listen 80; listen [::]:80;
server_name maven.xzsoft.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
EOF
ln -sf /etc/nginx/sites-available/maven /etc/nginx/sites-enabled/maven
rm -f /etc/nginx/sites-enabled/default && nginx -t && systemctl reload nginx
certbot certonly --webroot -w /var/www/html -d maven.xzsoft.com \
--non-interactive --agree-tos --register-unsafely-without-email
# 证书 90 天有效,certbot.timer 自动续期(依赖上面 80 的 ACME location,别删)2.3 面板建 https SNI 直通规则 → VM:443
curl -sk -X POST https://<面板>/panel/api/proxy -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"rule_type":"https","server_name":"maven.xzsoft.com","backend_host":"172.16.0.<octet>","backend_port":443,"machine_id":<id>,"enabled":true}'(https 规则是 SNI 直通,TLS 在 VM 的 nginx 终结)
第三部分:SSO 对接看门鹅
3.1 看门鹅侧:注册接入应用
管理后台 UI(或 API)创建应用:
- client_id:
nexus;机密客户端(生成 secret 记存密码管理) - redirect_uri:
https://maven.xzsoft.com/oauth2/callback - scopes:
openid,profile,email;grant:authorization_code,refresh_token
依赖的看门鹅能力(2026-09-18 版已内置,旧版需升级部署):
GET /api/auth/sso-logout:浏览器中心登出跳板(销毁会话 + BCL 扇出 + 302 登录页)- 授权端强制重认证:authorize 带
approval_prompt=force/prompt=login→ 销毁当前会话
→ 302 剥参 URL 重走流程(oauth2-proxy 每次授权自动带 force ⇒ "登出后访问必须重新输密码")
3.2 oauth2-proxy 容器
docker run -d --name oauth2-proxy --restart always --network nxnet -p 127.0.0.1:4180:4180 \
quay.io/oauth2-proxy/oauth2-proxy:v7.15.1 \
--provider=oidc \
--oidc-issuer-url=https://auth.xzsoft.com \
--client-id=nexus \
--client-secret=<看门鹅应用secret> \
--redirect-url=https://maven.xzsoft.com/oauth2/callback \
--cookie-secret=<恰16/24/32字节随机串> \
--cookie-name=_maven_sso \
--cookie-secure=true --cookie-samesite=lax \
--cookie-expire=0 \
--email-domain='*' \
--scope='openid profile email' \
--code-challenge-method=S256 \
--oidc-groups-claim=roles \
--reverse-proxy=true \
--pass-user-headers=true \
--set-xauthrequest=true \
--skip-provider-button \
--whitelist-domain='maven.xzsoft.com' \
--upstream=http://nexus:8081 \
--http-address=0.0.0.0:4180参数要点:--skip-provider-button 未登录直接跳 IdP;--cookie-expire=0 浏览器会话级
(关浏览器即需重登);--oidc-groups-claim=roles 接看门鹅应用级角色。
⚠️ 配置文件方式(--config)对格式极敏感,命令行参数最稳;cookie-secret 变更 = 全部 SSO 会话失效。
3.3 安装 SSO 插件(nexus-oauth2-proxy-plugin 3.6.1)
⚠️ 放 /deploy 目录无效(Nexus 的 Karaf 不扫描),必须手动挂载 feature:
# ① bundle 入 Karaf system repo(mvn 坐标布局)
docker exec -u 0 nexus mkdir -p /opt/sonatype/nexus/system/com/github/tumbl3w33d/nexus-oauth2-proxy-plugin/3.6.1
docker cp nexus-oauth2-proxy-plugin-3.6.1.jar nexus:/opt/sonatype/nexus/system/com/github/tumbl3w33d/nexus-oauth2-proxy-plugin/3.6.1/
docker cp nexus-oauth2-proxy-plugin-3.6.1-features.xml nexus:/opt/sonatype/nexus/system/com/github/tumbl3w33d/nexus-oauth2-proxy-plugin/3.6.1/
# jar 下载:https://github.com/tumbl3w33d/nexus-oauth2-proxy-plugin/releases/download/3.6.1/nexus-oauth2-proxy-plugin.kar
# (.kar 是 zip,用 jar tf / unzip 解出 repository/ 下两个文件)
# ② 改 community edition 的 features XML(⚠️ 容器层路径,容器重建需重做)
XML=/opt/sonatype/nexus/system/com/sonatype/nexus/assemblies/nexus-community-feature/3.77.2-02/nexus-community-feature-3.77.2-02-features.xml
# 聚合 feature 内追加一行:<feature version="3.6.1">nexus-oauth2-proxy-plugin</feature>
# 文件末尾 </features> 前追加:
# <feature name="nexus-oauth2-proxy-plugin" version="3.6.1">
# <bundle>mvn:com.github.tumbl3w33d/nexus-oauth2-proxy-plugin/3.6.1</bundle></feature>
# ③ 重启并验证 realm 出现
docker restart nexus
curl -s -u $NA "$NX/security/realms/available" | grep -o 'OAuth2ProxyRealm' && echo 插件OK3.4 激活 realm
curl -s -u $NA -X PUT "$NX/security/realms/active" -H 'Content-Type: application/json' \
-d '["OAuth2ProxyRealm","NexusAuthenticatingRealm","DefaultRole","DockerToken"]'3.5 nginx 443 完整配置(最终形态,含登出体系)
server {
listen 443 ssl; listen [::]:443 ssl;
http2 on;
server_name maven.xzsoft.com;
ssl_certificate /etc/letsencrypt/live/maven.xzsoft.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/maven.xzsoft.com/privkey.pem;
client_max_body_size 512m; # deploy 大构件
proxy_read_timeout 300s;
# Maven 协议:匿名 + Basic(deploy/个人token),直通 Nexus;信任头一律清空防外部伪造
location /repository/ {
proxy_pass http://127.0.0.1:8701;
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-Forwarded-User "";
proxy_set_header X-Forwarded-Email "";
proxy_set_header X-Forwarded-Preferred-Username "";
proxy_set_header X-Forwarded-Groups "";
}
# 管理/REST API:带 Authorization(脚本/CI)直通;无凭据(浏览器会话)转 oauth2-proxy
location /service/ {
if ($http_authorization = "") {
rewrite ^/service/(.*)$ /__proxy/service/$1 last;
}
proxy_pass http://127.0.0.1:8701;
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-Forwarded-User "";
proxy_set_header X-Forwarded-Email "";
proxy_set_header X-Forwarded-Preferred-Username "";
proxy_set_header X-Forwarded-Groups "";
}
location /__proxy/ {
internal;
rewrite ^/__proxy/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:4180;
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;
}
# Sign out 按钮接管:清本站双会话 cookie(UI 不自刷新,F5 后因看门鹅 force 重认证到登录页)
location = /service/rapture/session {
if ($request_method = DELETE) {
rewrite ^ /__signout last;
}
proxy_pass http://127.0.0.1:4180;
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;
}
location = /__signout {
internal;
add_header Set-Cookie "_maven_sso=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax" always;
add_header Set-Cookie "NXSESSIONID=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly" always;
return 204;
}
# 统一彻底登出入口(书签):清本站 cookie → 看门鹅中心登出 → 登录页
location = /logout {
add_header Set-Cookie "_maven_sso=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax" always;
add_header Set-Cookie "NXSESSIONID=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly" always;
return 302 https://auth.xzsoft.com/api/auth/sso-logout;
}
# oauth2-proxy 端点(登录跳转/回调)
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
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;
}
# UI 及其余浏览器流量:全部经 oauth2-proxy(未登录 302 看门鹅;已登录注入信任头转 Nexus)
location / {
proxy_pass http://127.0.0.1:4180;
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;
}
}3.6 服务器侧管理补充(3.77 无对应 REST 时用 extdirect + basic)
# capability 管理
curl -s -u $NA -X POST http://127.0.0.1:8701/service/extdirect -H 'Content-Type: application/json' \
-d '{"action":"capability_Capability","method":"read","type":"rpc","tid":1,"data":[]}'
# 登录 RPC(账号密码须 base64)
# {"action":"rapture_Security","method":"authenticate","type":"rpc","tid":2,"data":["<b64user>","<b64pass>"]}第四部分:登录/登出语义与权限
| 操作 | 行为 |
|---|---|
| 浏览器打开私服(无会话) | 302 看门鹅登录页(每次要求输密码:force 语义) |
| 同一浏览器会话内 | 保持登录(SSO cookie 会话级) |
| 点 Sign out + F5 | 看门鹅登录页要求重新输密码(实测终态) |
/logout 书签 |
彻底退出(本站 + 中心会话全销毁) |
| mvn 拉包 | 匿名(不走 SSO) |
| mvn 发布 | deploy 账号 或 个人 API Token(User→OAuth2 Proxy API Token 自助重置,仅显示一次) |
权限模型:SSO 用户权限 = 插件"外部角色映射"(Security→Roles→External Role Mapping,把看门鹅roles claim 的角色名映射到 nx-admin/nx-anonymous);直接给插件用户赋默认角色无效(插件 issue #26)。
新增可访问用户:先在 Nexus 预建同名账号并赋角色,陌生看门鹅账号默认 401(白名单制)。
第五部分:验证清单(装完自测)
curl -sI https://maven.xzsoft.com/repository/maven-public/ # 200(匿名拉包)
curl -sI -H 'Accept: text/html' https://maven.xzsoft.com/ # 302 → auth.xzsoft.com/oauth2/authorize
curl -s -u admin:<密码> https://maven.xzsoft.com/service/rest/v1/status # 200(basic 管理)
mvn -q clean compile # 客户端经 maven-public 拉包
# 浏览器:登录 → Sign out → F5 = 看门鹅登录页;/logout 后重开 maven 仍要求登录踩坑速查(本部署真实踩过,共 14 条)
| # | 现象 | 根因与解法 |
|---|---|---|
| 1 | 全站 403(admin 也 403) | 3.77 强制 EULA 未接受(§1.4②) |
| 2 | 插件放 /deploy 不加载 | Karaf 不扫描该目录;按 §3.3 手动挂 feature |
| 3 | Nexus 起不来(karaf.log 写入失败) | /data/nexus 属 root;chown 200:200 |
| 4 | oauth2-proxy 起不来 | cookie-secret 必须 16/24/32 字节 |
| 5 | oauth2-proxy 配置解析失败 | ini 格式对 CRLF/键名敏感;用命令行参数 |
| 6 | 未登录显示 Sign In 页不跳转 | 缺 --skip-provider-button |
| 7 | 登录后页面 401/匿名态 | 传给 Nexus 的身份标识取值不对(应以 preferred_username 为准) |
| 8 | Sign out "退不出去" | 三层合力缺一不可:nginx 拦截清 cookie + cookie 会话级 + 看门鹅 force 重认证 |
| 9 | 登出后自动回弹(signed out 又 signed in) | 看门鹅中心会话在 → 靠 force 重认证解决 |
| 10 | sso-logout 302 但会话没销毁 | sa-token token-prefix=Bearer 与 cookie 读取互斥;端点手动解析 cookie 裸值 |
| 11 | 登录 RPC "Authentication failed" | rapture 的账号密码参数是 base64;旧 cookie 会干扰(清缓存/换无痕) |
| 12 | UI 提示 Azure Blob Store Known Issue | 无关公告(非 Azure 部署),忽略 |
| 13 | 仓库创建 400 | 3.77 枚举差异:writePolicy=ALLOW_ONCE;更新接口路径带仓库名且 body 含 name |
| 14 | 同 IP 重建 VM 后 SSH 失败 | host key 变化,ssh-keygen -R(本机+跳板机) |
运维备忘
- 日志:
docker logs nexus / oauth2-proxy;nginx access log 可见全部分流(DELETE /service/rapture/session
返回 204 = Sign out 服务器侧已生效,别信页面表象) - 证书:certbot.timer 自动续期(勿删 80 的 ACME location)
- 备份:
/data/nexus整目录(构件/DB/插件用户) - 升级警示:禁止升级 Nexus 3.78+(插件失效);容器重建后需重做 §3.3(features XML 在容器层)
- 凭据存档:见 watchgoose 仓库 docs/private/生产部署拓扑.md(生产运维档,不入库)