使用 Casdoor 和 Oauth2-Proxy 保护内部应用

公司有许多面向内部的应用,这些应用有开源部署的也有自己开发的。我不想每个应用都要自行维护一套用户认证逻辑,而是使用统一的账号密码进行登录,也就是统一身份认证 CAS。

飞书也有提供 Oauth 授权,但是公司体量较小目前仍在白嫖,还没有开通商业套餐,就,用不了。。

飞书集成平台 - 先进连接方式,提升集成效率 (feishu.cn)

希望飞书商务可以向我们公司积极推销一下,我也想用 anycross 和 飞连啊 55555

于是转向了开源实现:

  1. 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
  2. Welcome | OAuth2 Proxy (oauth2-proxy.github.io)
  3. Module ngx_http_auth_request_module (nginx.org)

最终实现的效果:

Casdoor

安装

Casdoor 的安装十分方便,直接使用 docker 即可部署

(可选) 使用 Docker 运行 | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

在 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

Lark | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

获取登录用户信息 - 开发文档 - 飞书开放平台 (feishu.cn)

Casdoor 的文档里有 lark 的介绍,是通过企业自建应用授权请求飞书开放平台的 /open-apis/authen/v1/user_info 接口读取的用户信息。

其中,用户邮箱字段需要申请 获取用户邮箱信息 权限,并且这个邮箱不是分配的企业邮箱而是可以自定义的邮箱。

我在 Casdoor 中希望收集到用户的头像 & 名字 & 企业邮箱,就不能使用内置的 lark provider 了。(因为 lark provider 使用的是 email 字段的邮箱,还没地方改

于是转战自定义 provider。

自定义 Provider

Custom OAuth | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

在接口中获取企业邮箱需要先申请 获取用户雇佣信息 权限,返回的字段为 enterprise_email

自定义 Provider 需要按照文档中创建接口,我让 GPT 写了一个 node 进行 transform 。

transform 的作用是:

  1. 提供 access token 接口,请求飞书开放平台的 user access token 作为 access token 返回 casdoor
  1. 提供 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;
}
相关推荐
Victor3562 分钟前
MongoDB(42)如何使用$project阶段?
后端
Victor35610 分钟前
MongoDB(43)什么是嵌入式文档?
后端
Darkdreams36 分钟前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端
北寻北爱41 分钟前
vue2和vue3使用less和scss
前端·less·scss
bropro1 小时前
【Spring Boot】Spring AOP中的环绕通知
spring boot·后端·spring
lhbian1 小时前
【Spring Cloud Alibaba】基于Spring Boot 3.x 搭建教程
java·spring boot·后端
IT_陈寒1 小时前
Redis性能提升3倍的5个冷门技巧,90%开发者都不知道!
前端·人工智能·后端
LucianaiB1 小时前
OpenClaw 安装后必看!你真的会科学养虾吗?第1天和第47天的Openclaw有什么区别?
后端
雨雨雨雨雨别下啦1 小时前
Vue案例——面经
前端·javascript·vue.js
oo121382 小时前
里程碑5 - 完成框架 npm 包抽象封装并发布
前端·npm