Kong与SSO
kong的部署与基本配置
本文将以redhat系统为例,实现jwt登陆认证请求通过kong网关进行过滤,服务端无需进行jwt登陆认证校验操作
服务器建立两个无需认证的服务
- 安装服务运行环境
yum install python3
sudo pip3 install Flask==1.1.4
- 编写服务:本文使用Python的Flask框架作为后端服务
# 服务A
from flask import Flask, jsonify
app_a = Flask(__name__)
@app_a.route("/")
def hello_world():
return jsonify({'data': "server A"})
# # flask --app a run
if __name__ == '__main__':
app_a.run(host='0.0.0.0', port=5000)
# 服务B
from flask import Flask, jsonify
app_b = Flask(__name__)
@app_b.route("/")
def hello_world():
return jsonify({'data': "server B"})
# # flask --app b run
if __name__ == '__main__':
app_b.run(host='0.0.0.0', port=5001)
- 开启端口,此处使用centos虚拟机进行
sudo firewall-cmd --add-port=5000/tcp --permanent
sudo firewall-cmd --add-port=5001/tcp --permanent
sudo firewall-cmd --reload
安装Postgresql
kong需要数据版本大于9.5,yum直接安装的版本为9.2.24参考文章1参考文章2
补充:
Below is an example to install PostgreSQL 9.6 on RHEL/CentOS 7:
cat << EOF > /etc/yum.repos.d/pgdg-96.repo
[pgdg90]
name=PostgreSQL 9.6 RPMs for RHEL/CentOS 7
baseurl=https://yum-archive.postgresql.org/9.6/redhat/rhel-7-x86_64
enabled=1
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-PGDG
EOF
Now, you can install PostgreSQL 9.6 on RHEL/CentOS 7:yum install postgresql96-server
.
通过yum install postgresql96-devel
方便后续python安装包的处理
- 查看安装源
yum search postgresql
- 通过yum安装
# yum直装老版本 yum install postgresql-server (kong网关不可用)
# 安装12
# 添加安装包
sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# 上述步骤不成功时使用下面的步骤
curl -Lo pgdg-redhat-repo-latest.noarch.rpm $( https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm") || rpm -i pgdg-redhat-repo-latest.noarch.rpm
# 安装
sudo yum install -y postgresql12 postgresql12-server
# 安装17
sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo yum install -y postgresql17-server
- 检查安装版本 - 等初始化完成后在登录数据库
psql --version
- 初始化数据库 - 完成后会生成目录 /var/lib/pgsql/12/ (老版本*/var/lib/pgsql/data*),配置文件在其中
# yum直装老版本
postgresql-setup initdb
# 12
sudo /usr/pgsql-12/bin/postgresql-12-setup initdb
# 17
sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
- 启动服务
#启动PostgreSQL12服务
sudo systemctl start postgresql-12
#设置PostgreSQL12服务为开机启动
sudo systemctl enable postgresql-12
# 9.2版本 service postgresql start
# 9.6版本 sudo systemctl start postgresql-9.6
# 启动失败可尝试更新配置文件后再试
通过
netstat -nat
可看到运行端口 5432psql postgres
通过root用户登录,第一次会失败。使用psql -U postgres
以切换到postgres用户登录,会提示认证失败通过
su - postgres
切换到postgres用户后,执行psql
进行登录- 执行
su - postgres
出现密码,输入后提示 su: 鉴定故障 ,使用sudo su - postgres
来切换用户
- 执行
通过
vim /var/lib/pgsql/data/pg_hba.conf
修改配置
# 不切换用户登录 peer改为trust
local all all peer
# kong的迁移
host all all 127.0.0.1/32 trust
# 远程登录 - 增加如下行
host all all 0.0.0.0/0 md5
- 修改监听地址 - 非宿主机访问
vim /var/lib/pgsql/data/postgresql.conf
# 修改为 '*'
listen_addresses = 'localhost'
- 更新完配置后重启服务。可使用
psql -U postgres
直接登录,\q
退出
sudo systemctl restart postgresql-12
- 在postgresql内修改登录密码.
# 方法一
\password
# 方法二
alter user postgres with password 'target_password'
- 开启端口
sudo firewall-cmd --add-port=5432/tcp --permanent
sudo firewall-cmd --reload
补充:
\l
查看数据库\c
选择数据库\d
查看所有表格\d 表格名
查看指定表格- 创建表格
CREATE TABLE login_user(
phone CHAR(11) PRIMARY KEY NOT NULL,
username VARCHAR(15) NOT NULL,
password VARCHAR(15) NOT NULL,
level smallint DEFAULT 0
)
安装Kong
参考官方文档
- 安装nignx
yum install epel-release
yum update
yum install -y nginx
- 下载安装包 最新下载地址 https://cloudsmith.io/~kong/repos/gateway-legacy/packages/
# 该下载地址已不能使用
curl -Lo kong-enterprise-edition-3.3.0.0.rpm $( rpm --eval "https://download.konghq.com/gateway-3.x-rhel-%{rhel}/Packages/k/kong-enterprise-edition-3.3.0.0.rhel%{rhel}.amd64.rpm") || rpm -i
# 老版本 - 该下载地址已不能使用
curl -Lo kong-1.1.3.el7.noarch.rpm $( rpm --eval "https://download.konghq.com/gateway-1.x-centos-7/Packages/k/kong-1.1.3.el7.noarch.rpm") || rpm -i kong-1.1.3.el7.noarch.rpm
- 使用yum安装
sudo yum install kong-enterprise-edition-3.3.0.0.rpm
配置文件 /etc/kong/kong.conf.default
配置与启动
- 复制并修改 /etc/kong/kong.config.default 。
- 建立kong在postgresql内的数据库
CREATE USER kong WITH PASSWORD 'super_secret';
CREATE DATABASE kong OWNER kong;
grant all privileges on database kong to kong;
- 配置数据库的账号与密码
pg_user = kong
pg_password = password
- 迁移
KONG_PASSWORD="pwd" kong migrations bootstrap -c "PATH_TO_KONG.CONF_FILE"
- 启动
kong start -c "PATH_TO_KONG.CONF_FILE"
- 开放端口 - 8000服务api转发端口,8001kong管理端口
sudo firewall-cmd --add-port=8000/tcp --permanent
sudo firewall-cmd --reload
kong代理后端服务
- 声明一个需要kong接管的后端服务(记录标识名,服务名使用0.0.0.0)
$ curl -i -X POST \
--url http://localhost:8001/services/ \
--data 'name=服务标识符' \
--data 'url=http://服务地址'
如果访问 localhost 出现 *curl: (7) Failed to connect to ::1: 没有到主机的路由* ,说明localhost默认解析为ipv6的地址,使用 `curl -4 localhost:8001` 或 `curl 127.0.0.1`
转发路由:代理多个服务可使用两种方法进行
以host区分服务(为声明的服务添加路由,注意字段需为客户端请求的host,详见补充)
$ curl -i -X POST \ # 管理地址/services/服务标识符/routers --url http://localhost:8001/services/服务标识符/routes \ # 指明转发host规则:即后续请求头中携带指定host值字段将转发到对应的服务 --data 'hosts[]=example1.com'
补充::路由host字段的说明
- 此方法使用多个host区分服务,如
http://api.service_1.example.com/*
与http://api.service_2.example.com/*
来指向不同的服务 - 根据浏览器安全策略,浏览器发出的请求头中的Host字段根据为实际请求地址
http://{host}:{port|80}
中的host
值,且不可自定义。 - 在生产环境中,在一台服务器上部署kong网关在8000端口。如需转发多个后端请求,需全部请求到该服务器的8000端口,才可进入kong网关。
- 由于host值不可指定,而kong网关需要host值进行路由区分,因此该方法需为每个服务准备不同的域名,所有域名皆指向kong网关所在ip
- 此方法使用多个host区分服务,如
以一级路由区分:如
http://api.example.com/service_1/*
与http://api.example.com/service_2/*
来指向不同的服务
curl -i -X POST \ # 管理地址/services/服务标识符/routers --url http://localhost:8001/services/服务标识符/routes \ # 指明统一的转发host --data 'hosts[]=example.com' # 指明待转发服务的一级路由 --data 'paths[]=/service1/'
让kong转发到指定的服务
- host区分:配置测试服务A和B,以A为例
# 定义服务:服务标识符命名为service_a,本地服务的ip必须为0.0.0.0 curl -i -X POST --url http://localhost:8001/services/ \ --data 'name=service_a' \ --data 'url=http://0.0.0.0:5000' # 配置路由:添加kong的转发路由 curl -i -X POST --url http://localhost:8001/services/service_a/routes \ --data 'hosts[]=service_a.com'
- 一级路由区分:配置测试服务A和B,以A为例
# 服务标识符命名为service_a,本地服务的ip必须为0.0.0.0 curl -i -X POST --url http://localhost:8001/services/ \ --data 'name=service_a' \ --data 'url=http://0.0.0.0:5000' # 添加kong的转发路由 curl -i -X POST --url http://localhost:8001/services/service_a/routes \ --data 'hosts[]=service.com' \ --data 'paths[]=/service_a/'
路由的查看,修改,删除
# 查看
curl -i -X GET --url http://localhost:8001/services/服务标识符
# 修改
curl -i -X PATCH --url http://localhost:8001/services/服务标识符 \
--data 'name=服务的新标识符' \
--data 'retries=6' # 其他选项
# 删除
curl -i -X DELETE --url http://localhost:8001/services/服务标识符
- 代理完成后移除原服务端口,只对外暴漏kong端口通过代理请求对应服务
sudo firewall-cmd --remove-port=5000/tcp --permanent
sudo firewall-cmd --remove-port=5001/tcp --permanent
sudo firewall-cmd --reload
kong日志切分
kong日志不会主动切分,长时间运行日志往往很大,查看时加载时间较长。可考虑进行切分
- 安装 logrotate:在大多数 Linux 系统上,logrotate 工具通常已经预安装。如果没有安装,可以使用以下命令安装:
复制代码
sudo apt-get install logrotate # Debian/Ubuntu
sudo yum install logrotate # CentOS/RHEL
配置 logrotate
logrotate 的配置文件通常位于 /etc/logrotate.conf,而每个应用的单独配置文件通常放在 /etc/logrotate.d/ 目录下。我们可以为 Nginx 配置一个专门的日志切分规则
在 /etc/logrotate.d/ 目录下创建一个名为 kong 的配置文件,内容如下:
/data/logs/kong/gateway/*.log { create 0640 red root # 创建新日志文件时设置权限 daily # 每日切分 rotate 10 # 保留10个历史日志文件 missingok # 如果日志文件丢失,不报错 notifempty # 如果日志文件为空,不切分 compress # 对旧日志文件进行压缩 delaycompress # 延迟压缩,直到下一次轮换 sharedscripts # 确保在所有日志切分之后运行 postrotate postrotate # kong 日志切分后重载 kong /bin/kill -USR1 `cat /usr/local/kong/pids/nginx.pid 2>/dev/null` 2>/dev/null || true endscript }
- 同理可切分nginx日志:在 /etc/logrotate.d/ 目录下创建一个名为 nginx 的配置文件
/var/log/nginx/*.log { create 0640 nginx root daily rotate 10 missingok notifempty compress delaycompress sharedscripts postrotate /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true endscript }
执行切分
sudo /usr/local/bin/kong stop # 重启kong服务
sudo /usr/local/bin/kong start -c "/etc/kong/kong.conf"
sudo logrotate -d /etc/logrotate.conf # 查看配置的调试输出
sudo logrotate -f /etc/logrotate.conf # 强制执行日志切分
问题: 执行切分时出现 error: skipping "/data/logs/kong/gateway/access.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.
logrotate 为了防止潜在的安全风险,不允许操作所有用户可读写的文件。需要更新权限
# 调整目录权限与日志文件权限
sudo chmod 755 /<path to log file dir>
sudo chmod 644 /<path to log file>
kong网关多服务的jwt认证分析
通过kong的jwt插件可实现对用户的认证。作者思考到了两个设计方向可供选择:
- 利用jwt认证可以对服务或路由生效(该方法只能使用host区分服务)。由于服务中一些接口需要无权限也可访问,可将一个服务的接口设置两个kong服务。将需要认证的接口放置一个服务,并设置特殊路径,如
/auth/*
。将无需认证接口放置另一个服务,并设置特殊路径/free/*
。这样对kong来说,配置不同的服务,即可避免配置两个路由指向同一服务,需认证接口通过免认证路由请求而引起的安全性问题。(不同权限等级需不同一级路由,较为复杂) - 将全部数据接口与权限等级记入数据表中,根据路径与token中的用户权限查表鉴权,判断是否需要jwt认证,不需要直接放行后端,需要jwt认证的接口验证token得到用户权限,判断权限等级是否匹配。对token验证不通过的返回401,对权限不足的返回403。后续内容介绍的都是此方法的实现流程
建立需要不同用户权限的服务
# 服务A
from flask import Flask, jsonify
app_a = Flask(__name__)
@app_a.route('/')
def free():
return jsonify({'data': 'server A - level 0'})
@app_a.route('/auth')
def auth():
return jsonify({'data': 'server A - level 1'})
@app_a.route("/admin")
def admin():
return jsonify({'data': 'server A - level 2'})
# # flask --app a run
if __name__ == '__main__':
app_a.run(host='0.0.0.0', port=5000)
# 服务B
from flask import Flask, jsonify
app_b = Flask(__name__)
@app_b.route('/')
def free():
return jsonify({'data': 'server B - level 0'})
@app_b.route('/auth')
def auth():
return jsonify({'data': 'server B - level 1'})
@app_b.route('/admin')
def admin():
return jsonify({'data': 'server B - level 2'})
# # flask --app b run
if __name__ == '__main__':
app_b.run(host='0.0.0.0', port=5001)
部署插件
插件部署流程参考 csdn zz18435842675
kong的插件默认路径为 /usr/local/share/lua/5.1/kong/plugins
- /etc/kong/kong.conf 中配置插件路径
lua_package_path = /<path-to-plugin-location>/kong/plugins/?.lua;;
。其中:;;
代表默认路径;需确保最后两个路径为 /kong/plugins/ - 将插件文件夹放入指定目录(不生效时放入默认目录)
- 修改 /etc/kong/kong.conf 中的
plugins = bundled
,用逗号分隔在后方补充自定义插件 - 修改 /usr/local/share/lua/5.1/kong/constants.lua ,添加自定义插件
- 通过
kong reload -c "PATH_TO_KONG.CONF_FILE"
重启kong - 通过命令将插件绑定到服务
curl -X POST http://kong_ip:8001/services/<service-name-or-id>/plugins -d "name=my-custom-plugin"
插件开发
kong插件的文件结构文档
- handler.lua : 插件的主逻辑
- schema.lua : 配置信息。
- daos.lua : 数据库模型类。可自行建表后通过daos实例连接。如需迁移建表需要编写迁移文件。
下面将用jwt token的认证为例,说明一个接口认证插件的开发流程,源码在此。
框架搭建
一个最基本的kong插件需包含两个文件 handler.lua 与schema.lua
- handler的开发需按照以下格式进行
-- 必要的导包
local kong = kong
local BasePlugin = require "kong.plugins.base_plugin"
-- 声明自定义handler类
local RequestAuthHandler = BasePlugin:extend()
-- 定义优先级与版本号
RequestAuthHandler.PRIORITY = 800
RequestAuthHandler.VERSION = "1.0.0"
-- 自定义handler的new方法
function RequestAuthHandler:new()
-- 传入插件名
RequestAuthHandler.super.new(self, "kong_jwt_url_auth")
end
-- 主逻辑,access方法处理请求到来时的逻辑
function RequestAuthHandler:access(conf)
RequestAuthHandler.super.access(self)
-- 以下是主逻辑
-- 例如:pass options request
if kong.request.get_method() == "OPTIONS" then
return
end
end
-- 返回自定义handler
return RequestAuthHandler
- schema中的配置信息作为handler实例的传入参数。可通过传入参数调用配置信息
fields
中的预定义变量。配置项在绑定服务时传入了数据库,如后续更新,需要 删除plugins表中对应的项后重新进行绑定 (delete from plugins where name = '<plugin_name>' ;
)。
local typedefs = require "kong.db.schema.typedefs"
return {
name = "xxx", -- 插件名
fields = {
{ consumer = typedefs.no_consumer }, -- 插件的消费者
{ protocols = typedefs.protocols_http }, -- 插件协议
{ config = {
type = "record",
-- handler的配置信息 或 所需要的预定义变量
fields = {
{
-- jwt secret key
secret_key = { type = "string", default = "1234567891234567891234567891234567891234567" },
},
{
-- sign deliver, iss, not check
key_claim_name = { type = "string", default = "iss" },
},
},
},
},
},
}
权限认证
使用kong官方插件 jwt 中的jwt认证组件jwt_parser.lua进行验证
以下是在handler调用 jwt 插件内的认证组件并编写token认证逻辑
-- 导入所需内容
local get_header = kong.request.get_header
local set_header = kong.service.request.set_header
local jwt_decoder = require "kong.plugins.jwt.jwt_parser"
-- 省略
function RequestAuthHandler:access(conf)
RequestAuthHandler.super.access(self)
-- pass options request
if kong.request.get_method() == "OPTIONS" then
return
end
-- 0. 获取token
local bear_token = get_header("authorization")
if (bear_token == nil or string.sub(bear_token, 1, 7) ~= "Bearer ") then
kong.log.inspect("miss bearer")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
local token = string.sub(bear_token, 8, -1)
-- 1. 解码token
-- 1.1 base64解码token
local jwt, err = jwt_decoder:new(token)
if err then
-- 解码失败,token有问题
kong.log.inspect("decode error")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
-- 2. 验证
-- 2.1 获取payload
local claims = jwt.claims
-- 2.2 获取token header
local header = jwt.header
-- 2.3 获取私钥
-- local secret_key = "1234567891234567891234567891234567891234567"
kong.log.inspect(conf.secret_key)
-- 2.4 ***** 验签 *****
if not jwt:verify_signature(conf.secret_key) then
kong.log.inspect("check secret fail")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
-- 2.5 ***** 验证token有效期 *****
local ok_claims, errors = jwt:verify_registered_claims(conf.claims_to_verify)
if not ok_claims then
kong.log.inspect(errors)
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "Token Expired" })
end
-- 2.6 获取payload中的用户标识符
local phone = claims["phone"]
if phone == nil then
kong.log.inspect("miss phone in payload")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
kong.log.inspect(phone)
-- 3. 将用户标识符塞入header,方便后端服务查询对应用户信息
set_header("x-auth-phone", phone)
end
-- 省略
注意:
开发过程中可使用 kong.log.inspect()
进行关键信息打印,对复杂类型会进行格式化处理(性能开销),在生产环境中需要移除或使用kong.log.notice()
。
数据库查询
通过 daos.lua 中对目标表的 daos实例(模型类) 声明,以便在 handler.lua 中进行数据库查询。
关于数据库表生成的两种方式(与后端开发类似):
- 可通过 postgresql ctl 在kong连接的数据库中创建目标table,再根据table的字段定义daos实例
CREATE TABLE login_user( phone CHAR(11) PRIMARY KEY NOT NULL, username VARCHAR(15) NOT NULL, password VARCHAR(15) NOT NULL, level smallint DEFAULT 0); insert into login_user (phone, username, password, level) values ('13011111111', 'alice', '88888888', 1); CREATE TABLE api_mgr( sign VARCHAR(51) PRIMARY KEY, -- kong插件只可查询主键,主键需包含查询条件 path VARCHAR(50) NOT NULL, service smallint NOT NULL, auth_level smallint DEFAULT 0); insert into api_mgr (sign, path, service, auth_level) values ('0/', '/', 0, 0); insert into api_mgr (sign, path, service, auth_level) values ('0/auth', '/auth', 0, 1); insert into api_mgr (sign, path, service, auth_level) values ('0/admin', '/admin', 0, 2); insert into api_mgr (sign, path, service, auth_level) values ('1/', '/', 1, 0); insert into api_mgr (sign, path, service, auth_level) values ('1/auth', '/auth', 1, 1); insert into api_mgr (sign, path, service, auth_level) values ('1/admin', '/admin', 1, 2);
- 在 daos.lua 编写完成后,编写迁移文件 migrations/init.lua 与 migrations/000_base_<plugin_name>.lua 。通过kong的迁移命令在数据库中生成table。
daos.lua 实例
return {
{
primary_key = { "phone" },
name = "login_user", -- 数据表名称
endpoint_key = "phone", -- handler中的查询字段
cache_key = { "phone" }, -- 缓存字段
fields = {
-- 实例中的type为lua基本类型
{ phone = { type = "string", required = true, unique = true }, }, -- 主键 手机号作为用户唯一标识符
{ username = { type = "string", required = true }, },
{ password = { type = "string", required = true }, },
{ level = { type = "number", default = 0 }, }, -- 用户权限等级
},
},
}
- handler中的数据库查询核心代码
-- payload中获取用户标识符
local phone = claims["phone"]
if phone == nil then
kong.log.inspect("miss phone in payload")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
kong.log.inspect(phone)
-- 通过用户标识符查询用户对象 select用于查询主键
local user, err = kong.db.login_user:select({ phone = phone })
if err then
return error(err)
end
if not user then
kong.log.inspect("login user not found")
return kong.response.exit(401, { code = 401, success = false, data = "", msg = "UnAuthorized" })
end
-- 通过用户对象获取用户权限等级
set_header("x-auth-level", user.level)
账号多登录的判断
通过redis缓存token,kong插件查询token后进行判断
redis连接代码参考文章
- 服务端
# 服务端redis支持
# sudo pip3 install redis==3.5.3
# ** 登录接口中的判断 **
# 查询该用户之前是否存在有效期内的token
old_token = redis_conn.get('login:' + phone)
# 该用户存在已登录的token,将旧token标记为失效token
if old_token:
# 在token的最大过期时间内,该token都将标记为失效token
redis_conn.setex('conflict:' + old_token, 3 * 3600 * 24, 1)
# 记录该用户的最新token,下一次出现重复登录时可获取到旧token
redis_conn.setex('login:' + phone, 3 * 3600 * 24, token)
- 网关
local redis = require "resty.redis"
local redis_conn = redis:new()
redis_conn:set_timeouts(1000, 1000, 1000) -- 1 sec
local ok, err = redis_conn:connect(redisHost, redisPort)
if not ok then
kong.log.warn("failed to connect redis: ", err)
else
if(redisPwd ~= "")
then
local auth, err = redis_conn:auth(redisPwd)
if not auth then
kong.log.warn("failed to authenticate: ", err)
end
end
local conflict_res, err = redis_conn:get('conflict'..token)
if conflict_res ~= ngx.null then
kong.log.err("user "..phone.. " token conflict")
kong.response.exit(401, { code = 401, success = false, data = "", msg = "Token Conflict" })
return
end
-- 使用连接池
local ok, err = redis_conn:set_keepalive(10000, 100) -- (超时时间 ms, 连接池大小)
end
服务端适配
服务端生成token
通过PyJWT包进行token的生成,需要在payload中添加 exp
(过期时间)与nbf
(生成时间)
import jwt
secret_key = '1234567891234567891234567891234567891234567'
now = datetime.utcnow()
expiry = now + timedelta(days=3)
# 传入payload,私钥,加密算法
token = jwt.encode({'phone': '13011111111', 'exp': expiry, 'nbf': now}, secret_key, algorithm='HS256')
服务端对Postgresql的支持
通过
sudo pip3 install Flask-SQLAlchemy==2.5.1
安装Flask-SQLAlchemy。- 问题1: src/greenlet/greenlet.cpp:16:20: 致命错误:Python.h:没有那个文件或目录 。解决办法:
sudo yum install -y python3-devel
安装 - 问题2: inline T borrow() const G_NOEXCEPT 即gcc报错。 解决办法:升级pip
python3 -m pip install --upgrade pip
- 问题3: 服务端报错ModuleNotFoundError: No module named 'psycopg2' 。 解决办法: 安装psycopg2
- 问题1: src/greenlet/greenlet.cpp:16:20: 致命错误:Python.h:没有那个文件或目录 。解决办法:
通过
sudo pip3 install psycopg2-binary
安装psycopg2。- 问题: Error: pg_config executable not found. 。 解决办法: 确保安装 postgresql96-devel 后,在环境变量PATH中指明pg_config的位置 /usr/pgsql-9.6/bin
Flask连接Postgresql
import jwt
from datetime import datetime, timedelta
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
# 连接数据库
app_a = Flask(__name__)
app_a.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://kong:1234@127.0.0.1/kong'
app_a.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app_a)
# 模型类
class LoginUser(db.Model):
__tablename__ = 'login_user'
phone = db.Column(db.String(11), primary_key=True, doc='手机号')
username = db.Column(db.String(15), doc='昵称')
password = db.Column(db.String(15), doc='密码')
level = db.Column(db.SMALLINT, doc='用户权限')
def __repr__(self):
return '<User %r>' % self.phone
@app_a.route('/auth')
def auth():
phone = request.args.get("phone", '')
password = request.args.get('password', '')
if not all([phone, password]):
return jsonify({'success': False, 'msg': 'Missing required params!'})
# 查库进行账号认证
user = LoginUser.query.filter_by(phone=phone).first()
if not user:
return jsonify({'success': False, 'msg': 'User not exist!'})
if user.password != password:
return jsonify({'success': False, 'msg': 'Password incorrect!'})
return jsonify({'success': True, 'msg': 'server A - level 1', 'data': {'header': dict(request.headers)}})
# # flask --app a run
if __name__ == '__main__':
app_a.run(host='0.0.0.0', port=5000)
SSO服务端的完整代码
该服务端仅用于 token分发 及 接口权限管理
import jwt
from datetime import datetime, timedelta
from redis import StrictRedis
from flask import Flask, jsonify, request, current_app
from flask_sqlalchemy import SQLAlchemy
# 连接redis与postgresql
app_a = Flask(__name__)
app_a.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://kong:1234@127.0.0.1/kong'
app_a.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app_a)
app_a.redis_master = StrictRedis(host='127.0.0.1', port=6379, decode_responses=True)
class Role(db.Model):
__tablename__ = 'api_mgr'
sign = db.Column(db.String(51), primary_key=True, doc='api签名')
path = db.Column(db.String(50), doc='路径')
service = db.Column(db.SMALLINT, doc='服务ID')
auth_level = db.Column(db.SMALLINT, doc='api权限')
def __repr__(self):
return '<Api %r>' % self.sign
class LoginUser(db.Model):
__tablename__ = 'login_user'
phone = db.Column(db.String(11), primary_key=True, doc='手机号')
username = db.Column(db.String(15), doc='昵称')
password = db.Column(db.String(15), doc='密码')
level = db.Column(db.SMALLINT, doc='用户权限')
def __repr__(self):
return '<User %r>' % self.phone
@app_a.route('/')
def free():
# token生成
secret_key = '1234567891234567891234567891234567891234567'
now = datetime.utcnow()
expiry = now + timedelta(days=3)
token = jwt.encode({'phone': '13011111111', 'exp': expiry, 'nbf': now}, secret_key, algorithm='HS256')
# 测试对postgresql的支持
users = [{'phone': user.phone, 'username': user.username} for user in LoginUser.query.all()]
# 账号多登录的处理
old_token = current_app.redis_master.get('login:13011111111')
if old_token:
current_app.redis_master.setex('conflict:' + old_token, 3600*24*3, 1)
current_app.redis_master.setex('login:13011111111', 3600*24*3, token1)
return jsonify({
'success': True,
'msg': 'server A - level 0',
'data': {
'header': dict(request.headers),
'token': token,
'users': users
}
})
@app_a.route('/auth')
def auth():
phone = request.args.get("phone", '')
password = request.args.get('password', '')
if not all([phone, password]):
return jsonify({'success': False, 'msg': 'Missing required params!'})
# 测试对postgresql的支持
user = LoginUser.query.filter_by(phone=phone).first()
if not user:
return jsonify({'success': False, 'msg': 'User not exist!'})
if user.password != password:
return jsonify({'success': False, 'msg': 'Password incorrect!'})
return jsonify({'success': True, 'msg': 'server A - level 1', 'data': {'header': dict(request.headers)}})
@app_a.route("/admin")
def admin():
# 接口权限与用户权限不匹配的测试
return jsonify({'success': True, 'msg': 'server A - level 2', 'data': {'header': dict(request.headers)}})
# # flask --app a run
if __name__ == '__main__':
app_a.run(host='0.0.0.0', port=5000)
部署至生产服务器
多服务的多HOST域名部署
当设置多个不同的后端域名时(如协议不一致时)
客户端请求服务通过
api.*.*.com
,nginx监听80端口后转发到服务端8000端口来传入网关由于此时option请求无法到达后端,需在nginx统一配置跨域。此时后端无需再配置跨域信息
server {
listen 80;
server_name api.<service_sign>.*.com;
if ($request_method = OPTIONS ) {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,web-token,app-token,Authorization,Accept,Origin,Keep-Alive,User-Agent,X-Mx-ReqToken,X-Data-Type,X-Auth-Token,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
return 204;
}
location / {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:8000; # 转发给kong
proxy_http_version 1.1;
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-Port 80;
proxy_set_header Host $host;
}
}
多服务的单HOST域名部署
当设置一个统一域名,通过地址区分不同服务(协议必须统一)。如 api.*.com/a/*
用于请求A服务, api.*.com/b/*
用于请求B服务
- 配置主域名的nginx
server {
listen 80;
server_name api.*.com;
if ($request_method = OPTIONS ) {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,web-token,app-token,Authorization,Accept,Origin,Keep-Alive,User-Agent,X-Mx-ReqToken,X-Data-Type,X-Auth-Token,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
return 204;
}
# 服务a
location ~ ^/a/ {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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-Port 80;
proxy_set_header Host $host;
}
# 服务b
location ~ ^/b/ {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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-Port 80;
proxy_set_header Host $host;
}
}
- 配置kong通过路径进行路由转发
curl -i -X POST --url http://localhost:8001/services/<service_name>/routes --data 'name=<router_name>' --data 'hosts[]=api.*.com' --data 'paths[]=/<service_sign>/'
- 插件内通过
kong.router.get_route()
可直接判断需要转发的目标服务。以下为日志打印route信息
{
created_at = <route_create_timestamp>,
hosts = { "api.*.com",
<metatable> = <1>{
__class = {
__base = <table 1>,
__init = <function 1>,
__name = "PostgresArray",
<metatable> = {
__call = <function 2>,
__index = <table 1>
}
},
__index = <table 1>
}
},
id = "<route_id>",
name = "<route_name>",
paths = { "/<service_sign>/",
<metatable> = <table 1>
},
preserve_host = false,
protocols = { "http", "https",
<metatable> = {
__index = <function 3>
}
},
regex_priority = 0,
service = {
id = "<service_id>"
},
strip_path = true,
updated_at = <route_update_timestamp>
}
- 通过
kong.request.get_path()
获取路径,与路由的路径相剪以获取真实请求路径。根据接口管理判断请求路径是否存在。
-- 举例
prefix_path = kong.router.get_route().paths[1] -- /service_a/ lua的数组从1开始
path = kong.request.get_path() -- /service_a/test
-- 服务端得到的真实请求地址为 /test
r_path = string.gsub(str, "^".."/service_a/", tostring(service_id).."/")
插件开发中遇到的问题
在编辑完schema.lua后,加载插件时出现以下问题
[error] 2546#0: init_by_lua error: /usr/local/share/lua/5.1/kong/init.lua:402: error loading plugin schemas: on plugin 'kong_jwt_url_auth': [postgres] schema violation (fields.3: { fields = { [2] = { description = "unknown field" } } })
由报错信息可知,
descrption
字段不被支持。1.x版本的Kong在schema的配置中不支持descrption字段,新版本支持。插件源码中含有descrption
字段。