飛書 + Lua 實現企業級組織架構登入認證

highhand發表於2021-09-09

圖片描述

飛書是位元組跳動旗下一款企業級協同辦公軟體,本文將介紹如何基於飛書開放平臺的身份驗證能力,使用 Lua 實現企業級組織架構的登入認證閘道器。

登入流程

讓我們首先看一下飛書第三方網站免登的整體流程:

第一步: 網頁後端發現使用者未登入,請求身份驗證; 第二步: 使用者登入後,開放平臺生成登入預授權碼,302跳轉至重定向地址; 第三步: 網頁後端呼叫獲取登入使用者身份校驗登入預授權碼合法性,獲取到使用者身份; 第四步: 如需其他使用者資訊,網頁後端可呼叫獲取使用者資訊(身份驗證)。

圖片描述瀏覽器內網頁登入

Lua 實現

飛書介面部分實現

獲取應用的 access_token

function _M:get_app_access_token()
    local url = ""
    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 = ""
    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 = "" .. 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 git@github.com:ledgetech/lua-resty-http.git
git clone git@github.com:SkyLothar/lua-resty-jwt.git
git clone git@github.com: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 用於設定部門白名單(字串)

應用許可權說明

  • 獲取部門基礎資訊

  • 獲取部門組織架構資訊

  • 以應用身份讀取通訊錄

  • 獲取使用者組織架構資訊

  • 獲取使用者基本資訊


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/810/viewspace-2807088/,如需轉載,請註明出處,否則將追究法律責任。

相關文章