前言
prometheus是现在一个比较主流的监控软件,随着容器的普及,prometheus的应用越来越广泛,前面我也有专门讲到prometheus的相关文章。但是跟传统老牌的zabbix监控不一样,prometheus的web UI是没有登录认证的,有时候显得没有安全性,本文就主要讲解一下如何为prometheus添加一个登录认证界面。其实像elasticsearch、consul等一些服务的web UI也是没有登录认证的,都可以使用本文的方式进行实现。
1. 实现思路
主要是通过nginx代理转发进行实现,我们可以在nginx转发到prometheus之前添加一层认证的过程,从而进行实现。当然,如果有实力的朋友也可以修改prometheus的源码来添加认证机制。
1.1 nginx auth_basic 方式
nignx的ngx_http_auth_basic_module模块实现了访问nignx web页面必须输入用户名和密码,经过认证后才能进行访问,我们可以利用这个特性来为prometheus设置代理。
该实现方式比较简单,只需要在nginx配置文件里面添加上auth_basic相关的参数即可,网上也有很多资料,这里就不在赘述了。
1.2 nginx auth_request 方式
有时候我们需要自定义一个 web 登录网页作为我们的监控系统的登录入口,这就可以结合 auth_request 模块来实现。
auth_request原理:
(1)当auth_request对应的路由返回401或者403时,nginx会拦截请求,直接返回前端401或者403信息;
(2)当auth_request对应的路由返回2xx状态码时,nginx不会拦截请求,而是构建一个subrequest请求,再去请求真实受保护资源的接口;
登录认证实现思路:
(1)通过nginx代理prometheus访问,初次访问首页时,auth_request返回401,让其强制跳转到我们自定义的login 登录界面;
(2)在login登录认证界面,如果用户名密码认证正确,返回一个token,并且重定向到nginx首页;
(3)此时再次访问首页时,是带着token来进行访问,验证token正确,auth_request返回200,就成功转发 prometheus监控页面;
(4)如果token过期,登录首页时就返回到login页面,再次进行用户名密码认证。
2.实现代码
2.1 nginx配置文件
将如下配置添加到nginx的配置文件的 http{} 部分里面
server {
listen 0.0.0.0:9190; # 访问首页入口
location / {
proxy_pass http://localhost:9090/; # prometheus服务监听端口
auth_request /auth;
error_page 401 = @error401;
}
location @error401 { # 401就转发到登录页面
add_header Set-Cookie "ORIGINURL=$scheme://$http_host;Path=/";
return 302 /login;
}
location /auth {
# internal;
proxy_pass http://localhost:5000/auth; # 后端token认证
auth_request off;
}
location /login {
proxy_pass http://localhost:5000/login; # 后端用户名密码认证
auth_request off;
}
location /static/rainbowhhy { # 此处很重要,需要自定义一个静态文件目录,本文为rainbowhhy,否则会与prometheus的镜像文件冲突,导致prometheus的页面加载不完全
proxy_pass http://localhost:5000/static/rainbowhhy;
auth_request off;
}
}
2.2 登录认证
登录认证部分是通过 flask 实现
代码目录结构如下
├── profiles.json
├── readme.md
├── requirements.txt
├── run.py
├── static
│ └── rainbowhhy
│ ├── css
│ │ └── style.css
│ └── js
│ └── jquery-1.8.2.min.js
└── templates
└── login.html
安装包准备
pip3 install flask==1.1.1
pip3 install flask-login==0.4.1
pip3 install werkzeug==0.16.0
2.2.1 密码加密文件
profiles.json,采用json格式保存加密后的用户名和密码
cat profiles.json
{"admin": ["pbkdf2:sha256:150000$8J65mjTc$db116dd4d5de7eff899d126bd57b4f73910afb1e57982a9ded6878c547b584c5"]}
生成密码的方式:
>>> from werkzeug.security import generate_password_hash
>>> generate_password_hash("12345678")
'pbkdf2:sha256:150000$8J65mjTc$db116dd4d5de7eff899d126bd57b4f73910afb1e57982a9ded6878c547b584c5'
2.2.2 后端认证服务
run.py,实现了登录认证过程
from flask import Flask, request, render_template
from flask_login import UserMixin
from werkzeug.security import check_password_hash
import json
import os
app = Flask(__name__)
app.config["SECRET_KEY"] = "123456"
app.secret_key = '123456'
# 存放用户名和密码的json文件
PROFILE_PATH = os.path.dirname(os.path.abspath(__file__))
PROFILE_FILE = os.path.join(PROFILE_PATH, "profiles.json")
# 加密和存储
class User(UserMixin):
def __init__(self, username, password):
self.username = username
self.password_hash = self.get_password_hash()
def verify_password(self, password):
if self.password_hash is None:
return False
return check_password_hash(self.password_hash, password)
def get_password_hash(self):
"""从文件中获取密码"""
try:
with open(PROFILE_FILE) as f:
user_profiles = json.load(f)
user_info = user_profiles.get(self.username, None)
if user_info is not None:
return user_info[0]
except:
print("get password error!")
@app.route("/auth", methods=["GET", "POST"])
def auth():
url = request.cookies.get('ORIGINURL')
token = request.cookies.get('token')
if token == "ABCDE":
return ("success", 200)
else:
return ("go to login", 401)
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
user = User(username, password)
if user.verify_password(password):
token = "ABCDE"
return (token, 200)
else:
error = "用户名或密码错误..."
return (error, 403)
else:
return render_template("login.html")
if __name__ == '__main__':
app.config['JSON_AS_ASCII'] = False
app.run(host="localhost", port=5000)
2.2.3 前端登录页面
login.html,简单实现了登录认证的前端web
<!DOCTYPE html>
<html>
<head>
<title>监控系统</title>
<link type="text/css" rel="stylesheet" href="../static/rainbowhhy/css/style.css">
</head>
<body>
<div class="head">
账号:<input type="text" name="username" id="username" />
<br />
密码:<input type="password" name="password" id="password" />
<br />
<input type="button" onclick="token()" value="登录" />
<div class="flash error" role="alert"></div>
</div>
<script type="text/javascript" src="../static/rainbowhhy/js/jquery-1.8.2.min.js"></script>
<script type="text/javascript">
function token() {
var url = "http://" + document.domain + ":" + location.port;
console.log(url);
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
var fd = new FormData();
fd.append("username", username);
fd.append("password", password);
xhr = new XMLHttpRequest();
xhr.open("POST", "/login");
xhr.send(fd);
xhr.onreadystatechange = function (res) {
if (xhr.readyState == 4 && xhr.status == 200) {
// 登录成功则成功跳转
console.log("success");
var token = xhr.response;
setCookie("token", token);
location.href = url;
}
if (xhr.readyState == 4 && xhr.status == 403) {
// 登录失败则重新登录
var error = xhr.response;
$(".flash")[0].innerHTML = error;
$(".flash").fadeOut(3000);
setTimeout(function () {
location.href = url + "/login";
}, 2000);
}
}
}
function setCookie(name, value) {
// 设置token有效期为60min
const exp = new Date();
exp.setTime(exp.getTime() + 60 * 60 * 1000);
document.cookie = name + "=" + value + ";expires=" + exp.toGMTString();
}
</script>
</body>
</html>
style.css,为了体现出自定义一个静态文件目录的重要性,这里特地写了一个简单的自定义css
.head {
width: 500px;
height: 200px;
margin: 0 auto;
}
.error {
color:red;
font-size: 18px;
margin: 0 auto;
}
3.启动服务
启动nginx服务
systemctl start nginx
启动flask认证服务
python3 run.py
生产上可以做成systemd或者supervisor的方式启动
之后就可以访问了,实现的效果
文本实现了一个比较简单的登录认证页面,大家可以根据实际情况进行代码修改,使得login页面更加完善美观,当然如果你的公司没有要求这么多,就可以直接使用本文的思路中的第一种:nginx auth_basic 方式,更加方便快捷。
如下,上一张我们公司的登录界面:
参考文档
http://nginx.org/en/docs/http/ngx_http_auth_request_module.html