V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hsowan
V2EX  ›  Lua

飞书 + Lua 实现企业级组织架构登录认证

  •  4
     
  •   hsowan · 2021-08-14 11:53:08 +08:00 · 3815 次点击
    这是一个创建于 1205 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    登录流程

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

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

    浏览器内网页登录

    Lua 实现

    飞书接口部分实现

    获取应用的 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,表示对本项目的肯定与支持!

    4 条回复    2021-08-30 14:22:28 +08:00
    pooorguy
        1
    pooorguy  
       2021-08-14 12:42:26 +08:00
    最近在看 lua 和 openresty,刚好学学
    hsowan
        2
    hsowan  
    OP
       2021-08-14 13:10:53 +08:00
    @pooorguy 好啊,有问题可以相互讨论
    icbmicbm
        3
    icbmicbm  
       2021-08-16 09:49:29 +08:00
    惊了 在 luarocks 上刚看到 然后就刷到你了
    cy21st
        4
    cy21st  
       2021-08-30 14:22:28 +08:00
    大佬们,请教下你们用 lua 做什么业务呢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2520 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 05:00 · PVG 13:00 · LAX 21:00 · JFK 00:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.