飞书是字节跳动旗下一款企业级协同办公软件,本文将介绍如何基于飞书开放平台的身份验证能力,应用 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"], nilend

登录信息

JWT 登录凭证

咱们应用 JWT 作为登录凭证,同时用于保留用户的 open_iddepartment_ids

-- 生成 tokenfunction _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-- 验证与解析 tokenfunction _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 falseend

更多网关配置

同时反对 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/togit clone git@github.com:ledgetech/lua-resty-http.gitgit clone git@github.com:SkyLothar/lua-resty-jwt.gitgit 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 用于设置部门白名单(字符串)

利用权限阐明

  • 获取部门根底信息
  • 获取部门组织架构信息
  • 以利用身份读取通讯录
  • 获取用户组织架构信息
  • 获取用户根本信息

开源

本我的项目已实现且已在 GitHub 上开源:k8scat/lua-resty-feishu-auth,心愿大家能够动动手指导个 Star,示意对本我的项目的必定与反对!