公司有许多面向内部的应用,这些应用有开源部署的也有自己开发的。我不想每个应用都要自行维护一套用户认证逻辑,而是使用统一的账号密码进行登录,也就是统一身份认证 CAS。
飞书也有提供 Oauth 授权,但是公司体量较小目前仍在白嫖,还没有开通商业套餐,就,用不了。。
飞书集成平台 - 先进连接方式,提升集成效率 (feishu.cn)
希望飞书商务可以向我们公司积极推销一下,我也想用 anycross 和 飞连啊 55555
于是转向了开源实现:
- Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS
- Welcome | OAuth2 Proxy (oauth2-proxy.github.io)
- Module ngx_http_auth_request_module (nginx.org)
最终实现的效果:
Casdoor
安装
Casdoor 的安装十分方便,直接使用 docker 即可部署
在 docker 环境变量中配置好数据库的相关链接信息,然后在 nginx 中配置反向代理即可。
yaml
version: '3'
services:
main:
image: casbin/casdoor:latest
ports:
- 8000:8000
environment:
- RUNNING_IN_DOCKER=true
- driverName=mysql
- dataSourceName=xxxx:xxxx@tcp(host.docker.internal:3306)/
extra_hosts:
- host.docker.internal:host-gateway
restart: always
配置飞书的授权 Provider
获取登录用户信息 - 开发文档 - 飞书开放平台 (feishu.cn)
Casdoor 的文档里有 lark 的介绍,是通过企业自建应用授权请求飞书开放平台的 /open-apis/authen/v1/user_info
接口读取的用户信息。
其中,用户邮箱字段需要申请 获取用户邮箱信息
权限,并且这个邮箱不是分配的企业邮箱而是可以自定义的邮箱。
我在 Casdoor 中希望收集到用户的头像 & 名字 & 企业邮箱,就不能使用内置的 lark provider 了。(因为 lark provider 使用的是 email 字段的邮箱,还没地方改)
于是转战自定义 provider。
自定义 Provider
在接口中获取企业邮箱需要先申请 获取用户雇佣信息
权限,返回的字段为 enterprise_email
。
自定义 Provider 需要按照文档中创建接口,我让 GPT 写了一个 node 进行 transform 。
transform 的作用是:
- 提供 access token 接口,请求飞书开放平台的 user access token 作为 access token 返回 casdoor
- 提供 userinfo 接口,将飞书返回的 email 字段替换为 enterprise_email 字段
javascript
const express = require('express');
const axios = require('axios');
const app = express();
const port = 8000;
app.use(express.json());
app.use(require('body-parser').urlencoded({ extended: false }))
axios.defaults.baseURL = 'https://open.feishu.cn';
axios.interceptors.response.use((response) => {
if (response.data.code === 0) {
response.data = response.data?.data ?? response.data;
}
return response;
}, (error) => {
console.error(error)
return Promise.reject(error);
});
async function getAppAccessToken(appId, appSecret) {
try {
const response = await axios.post('/open-apis/auth/v3/app_access_token/internal', {
app_id: appId,
app_secret: appSecret,
});
return response.data.app_access_token;
} catch (error) {
throw new Error('Unable to retrieve app access token');
}
}
app.post('/open-apis/authen/v1/oidc/access_token', async (req, res) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader) {
return res.status(401).json({ error: 'Authorization header is missing' });
}
const decodedAuthHeader = Buffer.from(authorizationHeader.split(' ')[1], 'base64').toString('utf-8');
const [app_id, app_secret] = decodedAuthHeader.split(':');
try {
const appAccessToken = await getAppAccessToken(app_id, app_secret);
const response = await axios.post('/open-apis/authen/v1/oidc/access_token', {
grant_type: 'authorization_code',
code: req.body.code,
}, {
headers: {
Authorization: `Bearer ${appAccessToken}`, // 设置请求头的 Authorization
},
});
// 返回响应的 data 字段给客户端
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/open-apis/authen/v1/user_info', async (req, res) => {
const authorizationHeader = req.headers.authorization
if (!authorizationHeader) {
return res.status(401).json({ error: 'Authorization header is missing.' })
}
try {
const response = await axios.get('/open-apis/authen/v1/user_info', {
headers: {
authorization: authorizationHeader
}
})
const data = {
...response.data,
email: response.data.enterprise_email
}
return res.json(data)
} catch (error) {
return res.status(500).json({ error: error.message })
}
})
// 启动 Express 服务器
app.listen(port, () => {
console.log(`Express server is running on http://localhost:${port}`);
});
Casdoor 的 Provider 配置
Oauth2-Proxy
安装
Installation | OAuth2 Proxy (oauth2-proxy.github.io)
配置
text
http_address="127.0.0.1:8000"
# 表示 Oauth2-Proxy 运行在反向代理之后,使用 X-Real-IP 头,并允许X-Forwarded-{Proto,Host,Uri}在重定向选择上使用
reverse_proxy=true
# 使用 openssl rand -base64 16 生成
cookie_secret="qAfO37075T9xgs+uI+oBVw=="
cookie_domains=".example.com"
# 配置 Casdoor 为认证 provider
provider="oidc"
provider_display_name="Casdoor"
client_id="xxxx"
client_secret="xxxx"
# Casdoor 授权完成后的回调地址
# /oauth2/callback 是 oauth2-proxy 提供的接口
redirect_url="https://oauth.yuntu.chat/oauth2/callback"
# Casdoor 的地址
oidc_issuer_url="https://casdoor.example.com"
// 授权完成后允许跳转回的域名
whitelist_domains=".yuntu.chat"
// 授权的信息中 email 字段需要是此域的邮箱
email_domains=[
"example.com"
]
Nginx
Nginx 需要先安装 http_auth_request
模块,可以通过 nginx -V 检查,如果没有安装则需要重新编译 nginx 安装模块。
shell
root@xxxx:/# nginx -V
nginx version: nginx/1.24.0
built by gcc 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
built with OpenSSL 1.1.1q 5 Jul 2022
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/www/server/nginx
--add-module=/www/server/nginx/src/ngx_devel_kit
--add-module=/www/server/nginx/src/lua_nginx_module
--add-module=/www/server/nginx/src/ngx_cache_purge
--with-openssl=/www/server/nginx/src/openssl
--with-pcre=pcre-8.43 --with-http_v2_module
--with-stream --with-stream_ssl_module
--with-stream_ssl_preread_module
--with-http_stub_status_module
--with-http_ssl_module
--with-http_image_filter_module
--with-http_gzip_static_module
--with-http_gunzip_module
--with-ipv6
--with-http_sub_module
--with-http_flv_module
--with-http_addition_module
--with-http_realip_module
--with-http_mp4_module
--add-module=/www/server/nginx/src/ngx_http_substitutions_filter_module-master
--with-ld-opt=-Wl,-E
--with-cc-opt=-Wno-error
--with-http_dav_module
--add-module=/www/server/nginx/src/nginx-dav-ext-module
--with-http_auth_request_module
在 nginx 站点配置文件中配置 auth_request
text
auth_request /oauth2/auth;
error_page 401 = https://oauth.example.com/oauth2/sign_in?rd=$scheme://$host$request_uri;
在 nginx 站点配置文件中配置 /oauth2
的反向代理
text
location ^~ /oauth2/
{
proxy_pass https://oauth.example.com/oauth2/;
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 REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_http_version 1.1;
}
还可以编写一个 oauth.conf
文件,在有需要保护的站点直接 include 完成配置。
text
# oauth.conf
auth_request /oauth2/auth;
error_page 401 = https://oauth.example.com/oauth2/sign_in?rd=$scheme://$host$request_uri;
location ^~ /oauth2/
{
proxy_pass https://oauth.example.com/oauth2/;
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 REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_http_version 1.1;
}
text
# xxx.vhosts.conf
server
{
listen 80;
server_name xxx.exmaple.com;
include oauth.conf;
}