Basic Authentication 成为咱自建服务的主要鉴权方式已有快十个年头了。
在单点登录(SSO)普及的当下,自建服务的鉴权方式也该换了。
咱使用的 HTTP 服务器是 nginx,自建的服务通常由此反代后暴露在公网。
在 nginx 上使用 Basic Auth 非常简单,只需要一个密码文件及两行配置:
location /api {
auth_basic "Aki fukashi tonari wa nani mo shinai hito";
auth_basic_user_file /etc/nginx/.htpasswd;
}
经过一圈调研,发现其实单点登录的 nginx 实现也非常简单,同样是两行:
location /api {
auth_request /auth;
@error_page 401 = @error401;
}
熟悉 nginx 的选手应该已经意识到问题的不简单了:/auth
是什么? @error401
又是啥?
答案揭晓:
location /auth {
internal;
proxy_pass https://sso.sgdylan.com/auth;
# 反代 SSO 有 TLS 需带上这两行保证传递正确的 SNI 信息
proxy_ssl_server_name on;
proxy_ssl_name sso.sgdylan.com;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Origin-URI $host$request_uri;
proxy_set_header X-Host $http_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 @error401 {
return 302 https://sso.sgdylan.com/login?go=$scheme://$http_host$request_uri;
}
此处 sso.sgdylan.com
即是部署有单点登录服务的域名。
当用户访问到 /api
的时候,nginx 同时向 /auth
反代的 SSO 发送请求,此时得到响应码为 200
即允许用户访问。
不难发现,实现 SSO 服务其实非常简单,因此有着为数众多的轮子。
秉承最小化服务的原则,咱最终选定了 nginx-sso 作为 SSO 的实现。
选定这一方案的另外两点考量是为了完全兼容原有的 Basic Auth 及 Access Token,便于继续使用现有的配套自动化脚本,并同时支持 Yubikey 一键登录,实现现代化改造目标。
实现 Yubikey 或其他 Key 的一键登录有两种实现方法:
一是通过 WebAuthn,这需要一只 FIDO2/U2F 兼容的 Key(咱不是全部 Key 都支持);
二是使用 Key 的 2FA TOTP 作为身份验证使用(需要借助 Yubico 官方接口验证)。
WebAuthn 的好处是 Windows/Chrome 兼容性极佳,可以使用的介质方式非常多(指纹、人脸、手机、PIN 码等),但缺点是目前支持这一方式的实现都相对较重。
nginx-sso 使用的是第二种实现方法,有 Yubikey 的小伙伴是没问题的,但一大票廉价的国产 Key 就无缘了 (有生之年会实现的) 。
以下是无聊的配置环节:
nginx-sso 的配置文件 config.yaml
:
建议配合 repo 下的 Wiki 参考修改,部分内容需按注释重新产生
---
login:
title: "Loli Experiment - Login"
default_method: "simple"
hide_mfa_field: true
names:
simple: "Username / Password"
yubikey: "Yubikey"
cookie:
domain: ".sgdylan.com"
# You'll want to regenerate this. Use something like: cat /dev/urandom | tr -dc 'A-Za-z0-9' | dd bs=1 count=60
authentication_key: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
prefix: "sgdylan_sso"
secure: true
listen:
addr: "127.0.0.1"
port: 8022
audit_log:
targets:
- fd://stdout
- file:///var/log/nginx-sso/audit.jsonl
events: ['access_denied', 'login_success', 'login_failure', 'logout', 'validate']
headers: ['x-origin-uri']
trusted_ip_headers: ["X-Forwarded-For", "RemoteAddr", "X-Real-IP"]
acl:
rule_sets:
- rules:
- field: "x-origin-uri"
regexp: "edge.sgdylan.com/download.*"
# @_authenticated for all login user, @_anonymous for all user
allow: ["@users"]
- rules:
- field: "x-origin-uri"
equals: "edge.sgdylan.com/download.*"
allow: ["@users"]
providers:
simple:
enable_basic_auth: true
users:
# Bcrypt Mode: https://hostingcanada.org/htpasswd-generator/
admin: "$2y$10$gVMTIKKMJL5zstnlxgi9fOf6rOsNfVfswoC.yrjY3wKAnLX9O.dcy"
groups:
users: ["admin"]
yubikey:
# Get your client / secret from https://upgrade.yubico.com/getapikey/
client_id: "CLIENT_ID"
secret_key: "SECRET_KEY"
devices:
ccccdrfcvrhl: "admin"
groups:
users: ["admin"]
...
nginx 反代 nginx-sso:
server {
listen 80;
server_name sso.sgdylan.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2 default_server;
server_name sso.sgdylan.com;
location / {
proxy_pass http://127.0.0.1:8022/;
}
}
最后留意一点:
auth_request
所在的 location
块内如果是反代服务,需要设置允许 Referer 为 SSO 所在的域名,或者使用 proxy_set_header Referer "";
删掉由 SSO 跳转至服务域名带上的 Referer。