portal基于OIDC认证
1.OIDC介绍
OpenID Connect 简称为 OIDC,已成为 Internet 上单点登录和身份管理的通用标准。 它在 OAuth2 上构建了一个身份层,是一个基于 OAuth2 协议的身份认证标准协议。
OAuth2 实际上只做了授权,而 OpenID Connect 在授权的基础上又加上了认证。
OIDC 的优点是:简单的基于 JSON 的身份令牌(JWT),并且完全兼容 OAuth2 协议。
OpenID Connect 发布于 2014 年,是建立在 OAuth 2.0 协议之上的简单身份层,它允许客户端基于授权服务器或身份提供商(IdP)进行的身份验证来验证最终用户的身份,并获得用户的相关信息。 OpenID Connect 提供了 RESTful HTTP API,并使用 Json 作为数据的传递格式。
2.OIDC认证涉及的主要概念
Resource Owner - 用户
Resource Server - 服务器资源
Client - 用户前端程序
Authorization Server - 对用户认证并发送access_token
OIDC涉及的核心数据,这些关键数据可以和上面的demo相互印证
User Credential - 用户凭据(用户名和密码)
Client ID - client的唯一标识
Client Secret - Authroziation Server验证Client身份的标识
Authorization Code - 授权码,通过 User Credential + Client ID换取Authorization Code。授权码不能泄漏,且一次性有效。
Access Token - 访问令牌,拥有令牌者可以访问受保护的资源。通过 Authorization Code + Client ID + Client Secret 换取。Access Token在有效期内有效。
Refresh Token - 刷新令牌,重新获取(刷新)Access Token和Refresh Token。Refresh Token在有效期内有效。
ID Token - 包括会话认证的JWT,包括用户标识,identity provider,client信息
3.portal基于OIDC认证
3.1 安装keycloak
安装包下载:
wget https://github.com/keycloak/keycloak/releases/download/26.0.0/keycloak-26.0.0.tar.gz启动命令:
cd /root/keycloak/keycloak-26.0.0/binkeycloak 启动需要java:
java 安装方法参考:https://blog.csdn.net/lennonlau/article/details/146525998
配置环境变量
export JAVA_HOME=/usr/lib/jvm/jdk-17-oracle-x64
export PATH=$JAVA_HOME/bin:$PATH创建管理员账号账号(安装后设置一次就行):
./kc.sh bootstrap-admin user以上启动keycloak是不带证书的,需要带证书的,需要先生成证书 生成keycloak 需要的证书:
[root@CDN157 keycloak-26.0.0]#
[root@CDN157 keycloak-26.0.0]#
[root@CDN157 keycloak-26.0.0]# openssl genpkey -algorithm RSA -out privateKey.pem -pkeyopt rsa_keygen_bits:2048
......................................................................................................................................................................................+++
........................+++
[root@CDN157 keycloak-26.0.0]# openssl req -new -key privateKey.pem -out certificate.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:cn
State or Province Name (full name) []:beijing
Locality Name (eg, city) [Default City]:beijing
Organization Name (eg, company) [Default Company Ltd]:tmlake
Organizational Unit Name (eg, section) []:tmlake
Common Name (eg, your name or your server's hostname) []:keycloak
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[root@CDN157 keycloak-26.0.0]#
[root@CDN157 keycloak-26.0.0]# ls -ltr
总用量 28
-rw-r--r-- 1 root root 26 10月 4 2024 version.txt
-rw-r--r-- 1 root root 492 10月 4 2024 README.md
-rw-r--r-- 1 root root 11358 10月 4 2024 LICENSE.txt
drwxr-xr-x 5 root root 66 10月 4 2024 lib
drwxr-xr-x 2 root root 23 10月 4 2024 themes
drwxr-xr-x 2 root root 23 10月 4 2024 providers
drwxr-xr-x 3 root root 85 10月 4 2024 conf
-rw------- 1 root root 0 12月 22 11:39 nohup.out
drwxr-xr-x 4 root root 40 12月 22 11:40 data
drwxr-xr-x 3 root root 197 12月 22 14:02 bin
drwxr-xr-x 2 root root 6 12月 22 14:59 cert
-rw-r--r-- 1 root root 1704 12月 22 14:59 privateKey.pem
-rw-r--r-- 1 root root 1001 12月 22 15:01 certificate.csr
[root@CDN157 keycloak-26.0.0]# openssl x509 -req -days 365 -in certificate.csr -signkey privateKey.pem -out certificate.crt
Signature ok
subject=/C=cn/ST=beijing/L=beijing/O=tmlake/OU=tmlake/CN=keycloak
Getting Private key配置到conf/keycloak.conf 中
# The file path to a server certificate or certificate chain in PEM format.
https-certificate-file=/root/clb/keycloak/keycloak-26.4.4/cert/certificate.crt
# The file path to a private key in PEM format.
https-certificate-key-file=/root/clb/keycloak/keycloak-26.4.4/cert/privateKey.pem启动keycloak:
./kc.sh start-dev --http-port=8081访问地址
http: http://ip:8081/
https: https://ip:8443/
其中ip 是部署keycloak机器的ip
3.2 安装njet
portal_1.0.3.npk 和ssh_remote_mod.so 在other.tar.xz安装包里获取:
https://gitee.com/njet-rd/njet/releases/download/v4.0.2.0/other.tar.xz
yum localinstall njet-4.0.2.0-1.el7.x86_64.rpm更新数据库api_gateway.db 密码为123456
sudo /usr/local/njet/sbin/agw_passwd /usr/local/njet/apigw_data/api_gateway.db 123456配置:
njet.conf:
worker_processes auto;
cluster_name njet;
node_name node1;
error_log logs/error.log info;
helper ctrl modules/njt_helper_ctrl_module.so conf/njet_ctrl.conf;
helper broker modules/njt_helper_broker_module.so;
load_module modules/njt_agent_dynlog_module.so;
load_module modules/njt_dyn_ssl_module.so;
load_module modules/njt_http_vtsc_module.so;
load_module modules/njt_http_location_module.so;
load_module modules/njt_http_lua_module.so;
load_module modules/njt_http_dyn_upstream_module.so;
load_module modules/njt_http_dyn_server_module.so;
load_module modules/njt_http_token_sync_module.so;
load_module modules/njt_http_upstream_member_module.so;
load_module modules/njt_http_dyn_lua_module.so;
load_module modules/njt_openapi_util_module.so;
events {
worker_connections 1024;
}
shared_slab_pool_size 100m;
http {
token_sync zone=token:4M sync_time=5s clean_time=10s;
dyn_kv_conf conf/iot-work.conf;
include mime.types;
access_log logs/access.log;
vhost_traffic_status_zone;
lua_shared_dict mcp_message_bus 1M;
lua_package_path "$prefix/lualib/lib/?.lua;$prefix/modules/?.lua;$prefix/apps/?.lua;;";
lua_package_cpath "$prefix/lualib/clib/?.so;;";
init_by_lua_block {
local _=require("lor.index")
local _=require("lsqlite3complete")
}
server {
listen 8080 ssl;
error_page 401 =302 /portal;
client_max_body_size 1000m;
ssl_certificate /usr/local/njet/conf/test.com+1.pem;
ssl_certificate_key /usr/local/njet/conf/test.com+1-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
root html;
}
location /icons/ {
alias /usr/local/njet/apps/__icons/;
try_files $uri =404;
}
location /chat {
proxy_pass http://192.168.40.73/chat;
# 标准代理头
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;
# WebSocket 支持(如果聊天功能需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置(根据实际需要调整)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
}
location /_next {
sub_filter "Powered by" "";
proxy_pass http://192.168.40.73/_next;
# 标准代理头
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;
# WebSocket 支持(如果聊天功能需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置(根据实际需要调整)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
}
location /console {
proxy_pass http://192.168.40.73/console;
# 标准代理头
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;
# WebSocket 支持(如果聊天功能需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置(根据实际需要调整)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
}
location /api {
proxy_pass http://192.168.40.73/api;
# 标准代理头
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;
# WebSocket 支持(如果聊天功能需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置(根据实际需要调整)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
}
location /logo {
proxy_pass http://192.168.40.73/logo;
# 标准代理头
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;
# WebSocket 支持(如果聊天功能需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置(根据实际需要调整)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
}
}
upstream llm_backend {
server 0.0.0.0;
balancer_by_lua_block {
local balancer = require "njt.balancer"
local cjson = require "cjson"
local tokenLib = require("njt.token")
local data = njt.req.get_body_data()
local body_data = njt.req.get_body_data()
if not body_data then
njt.log(njt.ERR, "can not get body in balancer_by_lua")
else
local status, result = pcall(cjson.decode, body_data)
if not status then
njt.log(njt.ERR, "request body JSON decode error:".. body_data)
else
local token_key = "param_end_user_id_uuid_" .. result.user
local rc, tv_str = tokenLib.token_get(token_key)
if rc ~= 0 or not tv_str or tv_str == "" then
njt.log(njt.ERR, "in balance_by_lua user id is null of end_user_id:".. result.user)
else
njt.ctx.userid = tv_str
end
end
end
-- njt.log(njt.ERR, "=======upstream get body:", data)
-- local key = data or njt.var.remote_addr
-- local hash = njt.crc32_long(key) % 3
local servers = {
{ ip = "192.168.0.209", port = 8080 }
}
local server = servers[1]
local ok, err = balancer.set_current_peer(server.ip, server.port)
if not ok then
njt.log(njt.ERR, "Failed to set peer: ", err)
return njt.exit(500)
end
}
}
client_body_in_single_buffer on;
client_body_in_file_only off;
client_max_body_size 10m;
client_body_buffer_size 10m;
server {
listen 28080;
location /v1 {
mirror /llm_mirror;
#proxy_pass http://192.168.0.209:8080;
proxy_pass http://llm_backend;
header_filter_by_lua_block{
njt.ctx.res_tokens = 0
njt.ctx.stream_flag = false
local headers = njt.resp.get_headers()
if headers["Content-Type"] then
if string.find(headers["Content-Type"], "event-stream", 1, true) ~= nil then
njt.ctx.stream_flag = true
end
end
}
body_filter_by_lua_file /usr/local/njet/lua_filter/parse_response.lua;
}
location /llm_mirror {
internal;
content_by_lua_file /usr/local/njet/lua_filter/parse_request.lua;
}
}
server {
listen 28088;
location /njet_mcp {
content_by_lua_file /usr/local/njet/apps/njet_mcp/njet_mcp.lua;
}
}
}njet_ctrl.conf:
load_module modules/njt_http_sendmsg_module.so;
load_module modules/njt_ctrl_config_api_module.so;
load_module modules/njt_helper_health_check_module.so;
load_module modules/njt_http_upstream_api_module.so;
load_module modules/njt_http_location_api_module.so;
load_module modules/njt_doc_module.so;
load_module modules/njt_http_vtsd_module.so;
load_module modules/njt_http_lua_module.so;
load_module modules/njt_http_dyn_upstream_module.so;
load_module modules/njt_http_dyn_upstream_api_module.so;
load_module modules/njt_http_dyn_server_api_module.so;
load_module modules/njt_http_upload_module.so;
error_log logs/error_ctrl.log info;
events {
worker_connections 1024;
}
http {
dyn_kv_conf conf/ctrl_kv.conf;
lua_package_path "$prefix/lualib/lib/?.lua;$prefix/modules/?.lua;;";
lua_package_cpath "$prefix/lualib/clib/?.so;;";
init_by_lua_block {
local _=require("lor.index")
local _=require("lsqlite3complete")
}
include mime.types;
access_log off;
server {
client_max_body_size 1000m;
listen 8081;
location / {
return 200 "njet control panel\n";
}
location /api {
dyn_module_api;
}
location /doc {
doc_api;
}
location /metrics {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
}
location /api_gateway {
access_by_lua_block {
local ac=require("api_gateway.access.control")
local access=ac.new("/api_gateway")
access:check()
}
content_by_lua_block {
local api_gateway=require("api_gateway")
api_gateway.main()
}
}
}
}拷贝 ssh_remote_mod.so
sudo cp ssh_remote_mod.so /usr/local/njet/lualib/clib/启动njet:
sudo systemctl start njet 将下载后的 portal_1.0.3.npk 文件上传到 njet 主机上
NJET_SITE="http://localhost:8081"
PORTAL_FILE=$(curl -k -s -F "file=@portal_1.0.3.npk" "$NJET_SITE/api/v1/upload" |jq -r '.file')
echo $PORTAL_FILE
TOKEN=$(curl -k -X POST -d '{"login_data":{"username": "agw_admin", "password": "********"}}' -s "$NJET_SITE/api_gateway/auth/login" | jq -r '.token')
echo $TOKEN
curl -X 'POST' \
"$NJET_SITE/api_gateway/deploy/app" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"uploaded_file\": \"$PORTAL_FILE\"}"
浏览器输入:https://ip:8080/portal/ 进入portal 页面,在portal 页面 应用管理 页面可以上传需要部署的应用。
其中ip为部署njet 的机器
3.3 keycloak页面配置realm
3.4 keycloak页面配置client id
3.5 keycloak页面配置users
初始配置时user页面如果没有mobile 属性,需要增加自由属性mobile
3.5.1 初始配置增加user页面mobile 属性,步骤如下:
3.5.2 为属性添加映射
保存之后通过access token获取userinfo时就可以得到配置的该属性,该属性值在user页面可以设置
3.6 keycloak页面配置groups
3.7 keycloak页面将用户添加到上一步建的group 中
3.8 将group mapping映射到scope映射关系是在clients的应用里映射,名称为groups,勾选add to id token和add to userinfo
3.9 keycloak 地址配置:Keyclock 相关的 url, client_id, client_secret 可以存到 sys_config 表需要预先配置进去
| 字段名称 | value | 类型 | 描述 |
|---|---|---|---|
| keycloak_realm | tmlake | string | Keycloak 得realm, 对应portal的domain空格拆分可以保存多个,暂时使用一个 |
| keycloak_token_url | https://192.168.40.73:8443/realms/tmlake/protocol/openid-connect/token | string | 获取token url |
| keycloak_userinfo_url | https://192.168.40.73:8443/realms/tmlake/protocol/openid-connect/userinfo | string | 获取用户信息url |
| keycloak_client_id | bigModel | string | keycloak应用,此处创建的叫bigModel |
| keycloak_client_secret | RjdLTWVacktEV2d4aGVKeHVXZ0huNDJJUHRVRnFLeFE= | password | Client secret, keycloak自动生成, 此处不直接存储明文,目前使用base64简单加密,使用时在decode此处原值为F7KMeZrKDWgxheJxuWgHn42IPtUFqKxQ,进行base64加密后为RjdLTWVacktEV2d4aGVKeHVXZ0huNDJJUHRVRnFLeFE= base64可以这么生成(要使用-n参数,会去掉末尾的换行符)echo -n ’’ | base64 |
| keycloak_scope | openid,groups | string | 权限范围,此处包含用户基本信息和用户group信息,示例如下{“sub”:“c23afa76-c1d0-4980-ab00-9c041409b034”,“email_verified”:false,“max_tokens”:“5000”,“name”:“a bc”,“mobile”:“18201331979”,“groups”:["/guest_group"],“preferred_username”:“abc”,“given_name”:“a”,“family_name”:“bc”,“email”:“abc@tmlake.com”} |
| keycloak_grant_type | password | string | keycloak授权方式,用户名密码方式 |
获取token:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/auth/login' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"api_group_name": "app",
"login_type": "internal",
"login_data": {
"username": "agw_admin",
"password": "123456"
}
}'
返回:
{"token":"c644f5ee-19b7-4e24-8110-1c0ce22a8bfe","roles":[{"desc":"API Gateway admin ","id":1,"name":"agw_admin"}],"msg":"success","code":0,"user":{"name":"agw_admin","nickname":"","id":1}}keycloak_realm配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_realm",
"config_value": "njet.com",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_token_url配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_token_url",
"config_value": "https://部署keycloak机器的ip:8443/realms/njet.com.cn/protocol/openid-connect/token",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_userinfo_url 配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_userinfo_url",
"config_value": "https://部署keycloak机器的ip:8443/realms/njet.com.cn/protocol/openid-connect/userinfo",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_client_id 配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_client_id",
"config_value": "njetclient",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_client_secret 配置:
limin@limin-B660-VH:/usr/local/njet$ echo -n 'F7KMeZrKDWgxheJxuWgHn42IPtUFqKxQ' | base64
RjdLTWVacktEV2d4aGVKeHVXZ0huNDJJUHRVRnFLeFE=curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_client_secret",
"config_value": "RjdLTWVacktEV2d4aGVKeHVXZ0huNDJJUHRVRnFLeFE=",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_scope 配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_scope",
"config_value": "openid",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}keycloak_grant_type 配置:
curl -X 'POST' \
'http://192.168.40.215:8081/api_gateway/conf/sysconfig' \
-H 'accept: application/json' \
-H 'Authorization: Bearer c644f5ee-19b7-4e24-8110-1c0ce22a8bfe' \
-H 'Content-Type: application/json' \
-d '[
{
"config_key": "keycloak_grant_type",
"config_value": "password",
"config_type": "string"
}
]'
{
"msg": "success",
"data": "",
"code": 0
}3.10 登录portal页面,选中keycloak登录,验证是否可登录进去,log 日志有没有报错
keycloak 选中后会刷新 domain 列表接口
























