惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
爱范儿
爱范儿
T
The Exploit Database - CXSecurity.com
量子位
T
Tailwind CSS Blog
T
Threatpost
The GitHub Blog
The GitHub Blog
AWS News Blog
AWS News Blog
云风的 BLOG
云风的 BLOG
K
Kaspersky official blog
P
Proofpoint News Feed
博客园 - 司徒正美
L
LangChain Blog
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
S
Secure Thoughts
The Last Watchdog
The Last Watchdog
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
T
Troy Hunt's Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
W
WeLiveSecurity
A
Arctic Wolf
Apple Machine Learning Research
Apple Machine Learning Research
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Proofpoint News Feed
T
Tor Project blog
T
The Blog of Author Tim Ferriss
I
Intezer
P
Privacy & Cybersecurity Law Blog
美团技术团队
N
Netflix TechBlog - Medium
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Vulnerabilities – Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
G
Google Developers Blog
Attack and Defense Labs
Attack and Defense Labs
T
Tenable Blog
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
J
Java Code Geeks
腾讯CDC
Microsoft Security Blog
Microsoft Security Blog
A
About on SuperTechFans
Last Week in AI
Last Week in AI

Lua

用 Lua 写 Android 应用? AutoLua 做到了 - V2EX Android 上直接用 Lua 调 Java API 写脚本——AutoLua 技术分享 - V2EX 使用 Android Lua Helper 在 VSCODE 中调试安卓 Lua 应用 - V2EX skynet 怎么给 mysql drive 增加对 caching_sha2_password 的支持 - V2EX 奇怪的小问题 - V2EX Lua 代码上的优化,相互学习一下 - V2EX openresty + lua 这个地方怎么写 - V2EX 求一份 lua 入门教程 - V2EX 有关 Lua 调用 C++ 编译动态库程序 - V2EX 为什么游戏架构要用事件来驱动? - V2EX vscode 上写 lua 有显示注释的插件吗? - V2EX lua 为什么要使用~=当作不等于运算符? - V2EX 请问现在 lua 混淆用什么工具,求常年 lua 的老哥指点 - V2EX lua 的前景如何? - V2EX core_framework - 基于 libev 的轻量级 lua 网络开发框架 如何构建伪视频流服务? 低延时低采样, 基于 OpenResty , HTTP, 内网... TP-Link 官方使用的是开源固件? 表达过去、现在与将来:之将来(1) - V2EX 有个问题, lua 平时能用来做做什么 - V2EX ngx_lua 遇到的一个小坑 - V2EX Lua IDE 各位 V 友,用 lua 作为开发语言用什么 IDE 可以方便代码提示以及调试呢 - V2EX Lua 怎么实现 php strstr() 函数的功能 - V2EX 使用 Sol2 来进行 Lua 绑定 - V2EX luarocks 使用上的一些小技巧 - V2EX 正在用 lua 写一个 nginx 防盗链的拓展,遇到了一个问题。 - V2EX 9k-18k 宁波 经验 3-5 年 本科及以上 全职 职位诱惑 : 年终奖 出国旅游 顶级办公设备及环境 - V2EX 关于 Lua 的标准库 - V2EX Torch - V2EX 一个在 MediaWiki 里面加入 Lua 脚本的插件 - V2EX ngx_lua_reqstatus 实时监控 Nginx 域名 qps 的 lua 拓展 - V2EX 首页搜索到 14 个 python,现在应该是 15 个了,为神马没有 lua 一席之地 - V2EX 学习 ngx_lua - V2EX 用 Lua 写 Telegram 的 Bot - V2EX Lua 资料收集整理 - V2EX 云风翻译了 Lua 5.3 的手册 - V2EX 我对 Lua coroutine 的理解,希望有人能指点下有哪些更适合的使用场景或高阶用法~ - V2EX Lua 5.3 - V2EX 为什么 Lua 本身不包含像 endswith() 之类的特别常用的字符串函数呢? - V2EX 为 Atom 写了一个 linter-luacheck,希望大家喜欢 - V2EX lua 如何最快速度入门 - V2EX 学习 Lua,请问各位有什么书推荐? - V2EX lua 很牛啊,有人在服务端用 ta 么? - V2EX LÖVE - V2EX Scripting Nginx with Lua - V2EX 关于 openssl aes-256-cbc 的一些参数 - V2EX LuaJIT 2.1 PPA - V2EX 谁会Luci的cbi模块?怎样实现重启服务? - V2EX lua有没有3des加密解密的模块? - V2EX 关于学习 Lua 有什么好书推荐么? - V2EX lua好像有种简化的语法,叫啥来着 - V2EX
飞书 + Lua 实现企业级组织架构登录认证 - V2EX
hsowan · 2021-08-14 · via Lua

飞书是字节跳动旗下一款企业级协同办公软件,本文将介绍如何基于飞书开放平台的身份验证能力,使用 Lua 实现企业级组织架构的登录认证网关。

登录流程

让我们首先看一下飞书第三方网站免登的整体流程:

第一步: 网页后端发现用户未登录,请求身份验证; 第二步: 用户登录后,开放平台生成登录预授权码,302 跳转至重定向地址; 第三步: 网页后端调用获取登录用户身份校验登录预授权码合法性,获取到用户身份; 第四步: 如需其他用户信息,网页后端可调用获取用户信息(身份验证)。

浏览器内网页登录

飞书接口部分实现

获取应用的 access_token

function _M:get_app_access_token()
    local url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
    local body = {
        app_id = self.app_id,
        app_secret = self.app_secret
    }
    local res, err = http_post(url, body, nil)
    if not res then
        return nil, err
    end
    if res.status ~= 200 then
        return nil, res.body
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["tenant_access_token"]
end

通过回调 code 获取登录用户信息

function _M:get_login_user(code)
    local app_access_token, err = self:get_app_access_token()
    if not app_access_token then
        return nil, "get app_access_token failed: " .. err
    end
    local url = "https://open.feishu.cn/open-apis/authen/v1/access_token"
    local headers = {
        Authorization = "Bearer " .. app_access_token
    }
    local body = {
        grant_type = "authorization_code",
        code = code
    }
    ngx.log(ngx.ERR, json.encode(body))
    local res, err = http_post(url, body, headers)
    if not res then
        return nil, err
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["data"]
end

获取用户详细信息

获取登录用户信息时无法获取到用户的部门信息,故这里需要使用登录用户信息中的 open_id 获取用户的详细信息,同时 user_access_token 也是来自于获取到的登录用户信息。

function _M:get_user(user_access_token, open_id)
    local url = "https://open.feishu.cn/open-apis/contact/v3/users/" .. open_id
    local headers = {
        Authorization = "Bearer " .. user_access_token
    }
    local res, err = http_get(url, nil, headers)
    if not res then
        return nil, err
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["data"]["user"], nil
end

登录信息

JWT 登录凭证

我们使用 JWT 作为登录凭证,同时用于保存用户的 open_iddepartment_ids

-- 生成 token
function _M:sign_token(user)
    local open_id = user["open_id"]
    if not open_id or open_id == "" then
        return nil, "invalid open_id"
    end
    local department_ids = user["department_ids"]
    if not department_ids or type(department_ids) ~= "table" then
        return nil, "invalid department_ids"
    end

    return jwt:sign(
        self.jwt_secret,
        {
            header = {
                typ = "JWT",
                alg = jwt_header_alg,
                exp = ngx.time() + self.jwt_expire
            },
            payload = {
                open_id = open_id,
                department_ids = json.encode(department_ids)
            }
        }
    )
end

-- 验证与解析 token
function _M:verify_token()
    local token = ngx.var.cookie_feishu_auth_token
    if not token then
        return nil, "token not found"
    end

    local result = jwt:verify(self.jwt_secret, token)
    ngx.log(ngx.ERR, "jwt_obj: ", json.encode(result))
    if result["valid"] then
        local payload = result["payload"]
        if payload["department_ids"] and payload["open_id"] then
            return payload
        end
        return nil, "invalid token: " .. json.encode(result)
    end
    return nil, "invalid token: " .. json.encode(result)
end

使用 Cookie 存储登录凭证

ngx.header["Set-Cookie"] = self.cookie_key .. "=" .. token

组织架构白名单

我们在用户登录时获取用户的部门信息,或者在用户后续访问应用时解析登录凭证中的部门信息,根据设置的部门白名单,判断用户是否拥有访问应用的权限。

-- 部门白名单配置
_M.department_whitelist = {}

function _M:check_user_access(user)
    if type(self.department_whitelist) ~= "table" then
        ngx.log(ngx.ERR, "department_whitelist is not a table")
        return false
    end
    if #self.department_whitelist == 0 then
        return true
    end

    local department_ids = user["department_ids"]
    if not department_ids or department_ids == "" then
        return false
    end
    if type(department_ids) ~= "table" then
        department_ids = json.decode(department_ids)
    end
    for i=1, #department_ids do
        if has_value(self.department_whitelist, department_ids[i]) then
            return true
        end
    end
    return false
end

更多网关配置

同时支持 IP 黑名单和路由白名单配置。

-- IP 黑名单配置
_M.ip_blacklist = {}
-- 路由白名单配置
_M.uri_whitelist = {}

function _M:auth()
    local request_uri = ngx.var.uri
    ngx.log(ngx.ERR, "request uri: ", request_uri)

    if has_value(self.uri_whitelist, request_uri) then
        ngx.log(ngx.ERR, "uri in whitelist: ", request_uri)
        return
    end

    local request_ip = ngx.var.remote_addr
    if has_value(self.ip_blacklist, request_ip) then
        ngx.log(ngx.ERR, "forbided ip: ", request_ip)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    if request_uri == self.logout_uri then
        return self:logout()
    end

    local payload, err = self:verify_token()
    if payload then
        if self:check_user_access(payload) then
            return
        end

        ngx.log(ngx.ERR, "user access not permitted")
        self:clear_token()
        return self:sso()
    end
    ngx.log(ngx.ERR, "verify token failed: ", err)

    if request_uri ~= self.callback_uri then
        return self:sso()
    end
    return self:sso_callback()
end

使用

本文就不赘述 OpenResty 的安装了,可以参考我的另一篇文章《在 Ubuntu 上使用源码安装 OpenResty 》

下载

cd /path/to
git clone [email protected]:ledgetech/lua-resty-http.git
git clone [email protected]:SkyLothar/lua-resty-jwt.git
git clone [email protected]:k8scat/lua-resty-feishu-auth.git

配置

lua_package_path "/path/to/lua-resty-feishu-auth/lib/?.lua;/path/to/lua-resty-jwt/lib/?.lua;/path/to/lua-resty-http/lib/?.lua;/path/to/lua-resty-redis/lib/?.lua;/path/to/lua-resty-redis-lock/lib/?.lua;;";

server {
    access_by_lua_block {
        local feishu_auth = require "resty.feishu_auth"
        feishu_auth.app_id = ""
        feishu_auth.app_secret = ""
        feishu_auth.callback_uri = "/feishu_auth_callback"
        feishu_auth.logout_uri = "/feishu_auth_logout"
        feishu_auth.app_domain = "feishu-auth.example.com"

        feishu_auth.jwt_secret = "thisisjwtsecret"

        feishu_auth.ip_blacklist = {"47.1.2.3"}
        feishu_auth.uri_whitelist = {"/"}
        feishu_auth.department_whitelist = {"0"}

        feishu_auth:auth()
    }
}

配置说明

  • app_id 用于设置飞书企业自建应用的 App ID
  • app_secret 用于设置飞书企业自建应用的 App Secret
  • callback_uri 用于设置飞书网页登录后的回调地址(需在飞书企业自建应用的安全设置中设置重定向 URL )
  • logout_uri 用于设置登出地址
  • app_domain 用于设置访问域名(需和业务服务的访问域名一致)
  • jwt_secret 用于设置 JWT secret
  • ip_blacklist 用于设置 IP 黑名单
  • uri_whitelist 用于设置地址白名单,例如首页不需要登录认证
  • department_whitelist 用于设置部门白名单(字符串)

应用权限说明

  • 获取部门基础信息
  • 获取部门组织架构信息
  • 以应用身份读取通讯录
  • 获取用户组织架构信息
  • 获取用户基本信息

开源

本项目已完成且已在 GitHub 上开源:k8scat/lua-resty-feishu-auth,希望大家可以动动手指点个 Star,表示对本项目的肯定与支持!