飛書是字節跳動旗下一款企業級協同辦公軟件,本文將介紹如何基於飛書開放平臺的身份驗證能力,使用 Lua 實現企業級組織架構的登陸認證網關。linux
讓咱們首先看一下飛書第三方網站免登的總體流程:git
第一步: 網頁後端發現用戶未登陸,請求身份驗證;
第二步: 用戶登陸後,開放平臺生成登陸預受權碼,302跳轉至重定向地址;
第三步: 網頁後端調用獲取登陸用戶身份校驗登陸預受權碼合法性,獲取到用戶身份;
第四步: 如需其餘用戶信息,網頁後端可調用獲取用戶信息(身份驗證)。github
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
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
也是來自於獲取到的登陸用戶信息。redis
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 做爲登陸憑證,同時用於保存用戶的 open_id
和 department_ids
。json
-- 生成 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
ngx.header["Set-Cookie"] = self.cookie_key .. "=" .. token
咱們在用戶登陸時獲取用戶的部門信息,或者在用戶後續訪問應用時解析登陸憑證中的部門信息,根據設置的部門白名單,判斷用戶是否擁有訪問應用的權限。ubuntu
-- 部門白名單配置 _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 黑名單和路由白名單配置。segmentfault
-- 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 secretip_blacklist
用於設置 IP 黑名單uri_whitelist
用於設置地址白名單,例如首頁不須要登陸認證department_whitelist
用於設置部門白名單(字符串)本項目已完成且已在 GitHub 上開源:k8scat/lua-resty-feishu-auth,但願你們能夠動動手指點個 Star,表示對本項目的確定與支持!api