2021年8月

没有题头。

搭建CA

关于该部分,以后会详细阐述(大概,因为openssl有点难用),如果仍需接着实现本文内容,可以使用XCA这个图形化工具。

https://www.hohnstaedt.de/xca/

已更新,参见:

使用

只需要两行命令(http中):

# 假设你已经开启了https
# 客户端证书所属的根/中间CA证书
ssl_client_certificate /etc/nginx/ssl_cert/email_ca.crt;
# 开启客户端验证
ssl_verify_client on;
# 指定crl列表
ssl_crl /etc/nginx/ssl_cert/email_ca.crl;
#error_page 400 /req_cert.html;

400.jpg

(猫猫来源: https://http.cat/)

重启即可生效。不过只实现了简单的验证,后端无法知道客户端证书的主题信息,这时需要使用ngx_stream_ssl_module模块内置的一些变量,将它附加到头上传给后端即可。

#proxy_set_header ...;
# 客户端证书主题信息
add_header SSL_CLIENT_CERT_DN $ssl_client_s_dn;
# 判断验证状态(SUCCESS)
add_header SSL_CLINET_VERIFY $ssl_client_verify;

21082501.png

21082502.png

可以看到属性是以,分隔的。

ngx_stream_ssl_module模块变量表

(1.11.8起完全支持)

变量名说明
$ssl_cipher返回当前使用的加密套件
$ssl_ciphers返回使用客户端所支持的加密套件(已知名称列出,未知以十六进制显示)
$ssl_client_cert返回pem格式的客户端证书(除第一行外其余行末尾均有制表符)
$ssl_client_fingerprint返回客户端证书的sha1指纹
$ssl_client_i_dn返回客户端证书颁发者主题信息
$ssl_client_raw_cert返回pem格式的客户端证书
$ssl_client_s_dn返回客户端证书使用者主题信息
$ssl_client_serial返回客户端证书序列号
$ssl_client_v_end返回客户端证书截止日期
$ssl_client_v_remain返回客户端证书距离截止的天数
$ssl_client_v_start返回客户端证书颁发日期
$ssl_client_verify返回客户端证书验证状态("SUCCESS"/"FAILED:reason"/"NONE")
$ssl_curves返回客户端支持的ECC算法套件(已知名称列出,未知以十六进制显示)
$ssl_protocol返回已建立的ssl连接的协议
$ssl_server_name返回请求的sni名称
$ssl_session_id返回当前ssl会话标识符
$ssl_session_reusedssl会话复用标识(复用"r"/未复用".")

网上大部分是修改源码来实现,但我不喜欢折腾源码编译,所以使用了一些神奇的方法。

Nginx(Debian:nginx-full)中有两个神奇的指令,add_before_bodyadd_after_body,分别用来在响应内容前后插入内容,所以只要注入到末尾,用Javascript处理就行了。

location / {
    # ......
    add_after_body .uindex.html;
}

在目录下创建一个隐藏文件.uindex.html:

<!-- uindex  -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha512-Dop/vW3iOtayerlYAqCgkVr2aTr2ErwwTYOvRFUpzl2VhCMJyjQF0Q9TjUXIo6JhuM/3i0vVEt2e/7QQmnHQqw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
let dirlist = $("pre").html().split("\n");
function getFileInfo(line) {
    line = line.replace('    ', '|');
    line = line.replace('<a href="', '');
    line = line.replace('">', '|');
    line = line.replace('</a>', '|');
    line = line.split('|');
    temp = [];
    for(i in line) {
        if(line[i] != "") temp.push(line[i].replace(/(^\s*)|(\s*$)/g, ""));
    }
    line = null;
    //if(temp.length < 3) return [];
    return temp;
}
function outputHtml(dirlist) {
    let list = [];
    for(i in dirlist) {
        let temp = getFileInfo(dirlist[i]);
        if(temp) list.push(temp);
    }
    let temp = '';
    list.forEach((i) => {
        if(i.length >= 2) {
            if(i.length > 3) {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td>${i[2].replace(' -','')}</td><td>`;
                temp += `${i[3]}</td></tr>`;
            } else if(i.length == 2) {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td></td><td></td></tr>`;
            } else {
                temp += `<tr><td><a href="${i[0]}">${i[1]}</a></td><td>${i[2].replace(' -','')}</td><td>`;
                temp += `</td></tr>`;
            }
        }
    });
    return `<div class="container"><div class="row"><div class="col-md-12"><table class="table table-striped table-hover">
  <caption class="h3">${$("h1").text()}</caption>
  <thead><tr><th>fileName</th><th>date</th><th>size</th></tr></thead><tbody>${temp}</tbody></table></div></div></div><p style="text-align:center;font-size:14px;color:#dddddd;text-align:center;font-size:14px;color:#dddddd;">&copy; 2021 uindex.</p>`;
}
$(() => {
    //$("body").css({"display": "none"})
    let html = outputHtml(dirlist);
    $("h1").remove();
    $("hr").remove();
    $("pre").remove();
    $("body").append(html);
    //$("body").css({"display": "inline"})
});
</script>

不会正则,所以先替换再分割了一遍,旨在能用就行。使用时会一闪一下,正常。

21081701.png

一般情况下Nginx自带两个有关于鉴权的模块,一个是auth_basic,另一个是auth_request,本文主要介绍auth_request

流程

21080701.png

Nginx的配置

(server段中,也可以做成一个配置文件,通过include引用)

#设置401错误跳转,用于跳转到登录页
location @error401 {
    #这里的url方便我们登录成功的跳转
    return 302 https://auth.example.com/?url=https://$http_host$request_uri;
}
#内部路由
location /auth {
    internal;
    #鉴权服务器地址
    proxy_pass http://127.0.0.1:8888/auth;
    #不传递body内容,当然请求头会被传递
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

(location段中)
location / {
    #指定内部路由与返回401的处理
    auth_request /auth;
    error_page 401 = @error401;
}

鉴权验证

401.jpg
(猫猫来源: https://http.cat/)

Nginx会通过所返回的状态码来判断是否放行请求,返回2XX放行,返回4XX则拦截请求,我们可以通过设置cookie来保证登录态的持续:

(Python3)
from sanic import Sanic, html, json, redirect
app = Sanic(__name__)

password = "19260817"

@app.get("/")
async def index(request):
    return html("<form action="" method="POST"><input name="pw"><button type="submit">check</button></form>")

@app.post("/")
async def vindex(request):
    pw = request.form.get("pw", None)
    url = request.args.get("url", None)
    if pw == password:
        res = redirect(url)
        res.cookies["login"] = True
        res.cookies["login"]["httponly"] = True
        #全域cookie
        res.cookies["login"]["domain"] = ".example.com"
        return res
    return redirect("/")

@app.get("/auth")
async def auth(request):
    login = request.cookies.get("login", False)
    if login:
        return json({"code": 200}, 200)
    return json({"code": 401}, 401)

if __name__ == "__main__":
    app.run(port=8888)