飞书开发学习笔记(六)-网页应用免登
一.上一例的问题修正
在上一例中,飞书登录查看网页的界面显示是有误的,看了代码,理论上登录成功之后,应该显示用户名等信息。
最后的res.nickName是用户名,res.i18nName.en_us是英文名
function showUser(res) {
// 展示用户信息
// 头像
$("#img_div").html(
`<img src="${res.avatarUrl}" width="100%" height=""100%/>`
);
// 名称
$("#hello_text_name").text(
lang === "zh_CN" || lang === "zh-CN"
? `${res.nickName}`
: `${res.i18nName.en_us}`
);
// 欢迎语
$("#hello_text_welcome").text(
lang === "zh_CN" || lang === "zh-CN" ? "欢迎使用飞书" : "welcome to Feishu"
);
}
而这个页面结果显然是不正确的
仔细检查Console.log,原来是接收config参数时出现了错误,python使用了分步调试,发现问题出在
ticket = auth.get_ticket()
再进一步分析变量,发现是
APP_ID = os.getenv("APP_ID")
APP_SECRET = os.getenv("APP_SECRET")
FEISHU_HOST = os.getenv("FEISHU_HOST")
三个变量获取错误,最后锁定源头是.env中三个变量定义以后没有换行,应该是在查看后不小心修改了格式保存引起的,在改为标准格式,每个变量定义后换行,重新读入,成功获取用户名等信息,
这也是一个小插曲,对于学习来说却耽误了一个小时。
这才是登录成功的界面
二.应用免登
紧接着上一案例,就是应用免登的介绍
应用免登:应用免登
免登流程
网页应用免登为网页应用提供了获取当前登录用户的飞书身份的能力,网页应用免登流程如下:
步骤一:获取用户登录预授权码。
步骤二:使用预授权码获取user_access_token。
步骤三:获取用户信息并完成登录。
步骤四:刷新已过期的user_access_token。
实际应用分为四步,每个步骤与上一例基本相同。
不同点为第三步,配置网页应用访问地址时,需要同时填写
进入飞书开放平台: https://open.feishu.cn/app
完成重定向URL填写
从工作台打开应用后,同时获取用户基本信息
三.代码结构
3.1代码结构树
|── README.zh.md ----- 说明文档
|── doc_images ----- 说明文档的图片资源
|── public
| |── svg ----- 前端图形文件
| |──index.css ----- 前端展示样式
| |── index.js ----- 前端主要交互代码(免登流程函数、用户信息展示函数)
|── templates
| |──err_info.html ----- 前端错误信息展示页面
| |── index.html ----- 前端用户信息展示页面
|── auth.py ----- 服务端免登流程类、错误信息处理类
|── server.py ----- 服务端核心业务代码
|── requirements.txt ----- 环境配置文件
└── .env ----- 全局默认配置文件,主要存储 App ID 和 App Secret 等
3.2 index.html ----- 前端用户信息展示页面
在页面内部的js中,利用login_info判断是否已经登陆,如果已经登陆,就直接展示;否则走登陆程序
判断则转到index.js
<!DOCTYPE html>
<link rel="stylesheet" href="/public/index.css"/>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>网页应用</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
</head>
<!-- 在html文档中引入 JSSDK -->
<!-- JS 文件版本在升级功能时地址会变化,如有需要(比如使用新增的 API),请重新引用「网页应用开发指南」中的JSSDK链接,确保你当前使用的JSSDK版本是最新的。-->
<script type="text/javascript"
src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.16.js">
</script>
<!-- 在页面上添加VConsole方便调试-->
<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
var vConsole = new window.VConsole();
</script>
<body>
<div>
<div class="img">
<!-- 头像 -->
<div id="img_div" class="img_div"> </div>
<span class="hello_text">Hello</span>
<!-- 名称 -->
<div id="hello_text_name" class="hello_text_name"></div>
<!-- 欢迎语 -->
<div id="hello_text_welcome" class="hello_text_welcome"></div>
</div>
<!-- 飞书icon -->
<div class="icon"><img src="../public/svg/icon.svg"/></div>
</div>
<!-- 在html文档中引入本demo免登流程函数apiAuth()和用户信息展示函数showUser(res) -->
<script src="/public/index.js"></script>
<!-- 根据服务端传来的参数login_info判断走免登流程还是直接展示用户信息 -->
<script>
const login_info = '{{ login_info }}';
console.log("login info: ", login_info);
if (login_info == "alreadyLogin") {
const user_info = JSON.parse('{{ user_info | tojson | safe }}');
console.log("user: ", user_info.name);
$('document').ready(showUser(user_info))
} else {
$('document').ready(apiAuth())
}
</script>
</body>
</html>
3.3 index.js ----- 前端主要交互代码(免登流程函数、用户信息展示函数)
服务器端Route:server.py
1.服务器端执行get_appid获取app_id
2.调用JSAPI tt.requestAuthCode 获取 authorization code,参数为app_id,存储在 res.code
3.服务器端fetch把code传递给接入方服务端Route: callback,并获得user_info
4.成功后showUser(res)
let lang = window.navigator.language;
console.log(lang);
function apiAuth() {
if (!window.h5sdk) {
console.log('invalid h5sdk')
alert('please open in feishu')
return
}
// 通过服务端的Route: get_appid获取app_id
// 服务端Route: get_appid的具体内容请参阅服务端模块server.py的get_appid()函数
// 为了安全,app_id不应对外泄露,尤其不应在前端明文书写,因此此处从服务端获取
fetch(`/get_appid`).then(response1 => response1.json().then(res1 => {
console.log("get appid succeed: ", res1.appid);
// 通过error接口处理API验证失败后的回调
window.h5sdk.error(err => {
throw('h5sdk error:', JSON.stringify(err));
});
// 通过ready接口确认环境准备就绪后才能调用API
window.h5sdk.ready(() => {
console.log("window.h5sdk.ready");
console.log("url:", window.location.href);
// 调用JSAPI tt.requestAuthCode 获取 authorization code
tt.requestAuthCode({
appId: res1.appid,
// 获取成功后的回调
success(res) {
console.log("getAuthCode succeed");
//authorization code 存储在 res.code
// 此处通过fetch把code传递给接入方服务端Route: callback,并获得user_info
// 服务端Route: callback的具体内容请参阅服务端模块server.py的callback()函数
fetch(`/callback?code=${res.code}`).then(response2 => response2.json().then(res2 => {
console.log("getUserInfo succeed");
// 示例Demo中单独定义的函数showUser,用于将用户信息展示在前端页面上
showUser(res2);}
)
).catch(function (e) {console.error(e)})
},
// 获取失败后的回调
fail(err) {
console.log(`getAuthCode failed, err:`, JSON.stringify(err));
}
})
}
)
})).catch(function (e) { // 从服务端获取app_id失败后的处理
console.error(e)
})
}
function showUser(res) {
// 展示用户信息
// 头像
$('#img_div').html(`<img src="${res.avatar_url}" width="100%" height=""100%/>`);
// 名称
$('#hello_text_name').text(lang === "zh_CN" || lang === "zh-CN" ? `${res.name}` : `${res.en_name}`);
// 欢迎语
$('#hello_text_welcome').text(lang === "zh_CN" || lang === "zh-CN" ? "欢迎使用飞书" : "welcome to Feishu");
}
3.4 auth.py ----- 服务端免登流程类、错误信息处理类
1.获取authorize_app_access_token,参数为 “app_id” 和 “app_secret”,获取到USER_ACCESS_TOKEN_URI
2.获取authorize_user_access_token,url 通过self._gen_url(USER_ACCESS_TOKEN_URI)获得
3.authorize_user_access_token的headers中 :“app_access_token” 位于HTTP请求的请求头
4.response.json().get(“data”)获得user_access_token
def authorize_app_access_token(self):
# 获取 app_access_token, 依托于飞书开放能力实现.
# 文档链接: https://open.feishu.cn/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/app_access_token_internal
url = self._gen_url(APP_ACCESS_TOKEN_URI)
# "app_id" 和 "app_secret" 位于HTTP请求的请求体
req_body = {"app_id": self.app_id, "app_secret": self.app_secret}
response = requests.post(url, req_body)
Auth._check_error_response(response)
self._app_access_token = response.json().get("app_access_token")
# 这里也可以拿到user_info
# 但是考虑到服务端许多API需要user_access_token,如文档:https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-overview
# 建议的最佳实践为先获取user_access_token,再获得user_info
# user_access_token后续刷新可以参阅文档:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/authen/refresh_access_token
def authorize_user_access_token(self, code):
# 获取 user_access_token, 依托于飞书开放能力实现.
# 文档链接: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/authen/access_token
self.authorize_app_access_token()
url = self._gen_url(USER_ACCESS_TOKEN_URI)
# “app_access_token” 位于HTTP请求的请求头
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + self.app_access_token,
}
# 临时授权码 code 位于HTTP请求的请求体
req_body = {"grant_type": "authorization_code", "code": code}
response = requests.post(url=url, headers=headers, json=req_body)
Auth._check_error_response(response)
self._user_access_token = response.json().get("data").get("access_token")
def get_user_info(self):
# 获取 user info, 依托于飞书开放能力实现.
# 文档链接: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/authen/user_info
url = self._gen_url(USER_INFO_URI)
# “user_access_token” 位于HTTP请求的请求头
headers = {
"Authorization": "Bearer " + self.user_access_token,
"Content-Type": "application/json",
}
response = requests.get(url=url, headers=headers)
Auth._check_error_response(response)
# 如需了解响应体字段说明与示例,请查询开放平台文档:
# https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/authen/access_token
return response.json().get("data")
3.5 server.py ----- 服务端核心业务代码
1.关键函数callback():
2.通过传递的code先获取 user_access_token
3. 再获取 user info
4. 将 user info 存入 session
5. get_home()查询USER_INFO_KEY 是否在session,在则免登,否则就登陆程序
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import logging
from auth import Auth
from dotenv import load_dotenv, find_dotenv
from flask import Flask, render_template, session, jsonify, request
# 日志格式设置
LOG_FORMAT = "%(asctime)s - %(message)s"
DATE_FORMAT = "%m/%d/%Y %H:%M:%S"
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)
# const
# 在session中存储用户信息 user info 所需要的对应 session key
USER_INFO_KEY = "UserInfo"
# secret_key 是使用 flask session 所必须有的
SECRET_KEY = "ThisIsSecretKey"
# 从 .env 文件加载环境变量参数
load_dotenv(find_dotenv())
# 初始化 flask 网页应用
app = Flask(__name__, static_url_path="/public", static_folder="./public")
app.secret_key = SECRET_KEY
app.debug = True
# 获取环境变量值
APP_ID = os.getenv("APP_ID")
APP_SECRET = os.getenv("APP_SECRET")
FEISHU_HOST = os.getenv("FEISHU_HOST")
# 用获取的环境变量初始化免登流程类Auth
auth = Auth(FEISHU_HOST, APP_ID, APP_SECRET)
# 业务逻辑类
class Biz(object):
@staticmethod
def home_handler():
# 主页加载流程
return Biz._show_user_info()
@staticmethod
def login_handler():
# 需要走免登流程
return render_template("index.html", user_info={"name": "unknown"}, login_info="needLogin")
@staticmethod
def login_failed_handler(err_info):
# 出错后的页面加载流程
return Biz._show_err_info(err_info)
# Session in Flask has a concept very similar to that of a cookie,
# i.e. data containing identifier to recognize the computer on the network,
# except the fact that session data is stored in a server.
@staticmethod
def _show_user_info():
# 直接展示session中存储的用户信息
return render_template("index.html", user_info=session[USER_INFO_KEY], login_info="alreadyLogin")
@staticmethod
def _show_err_info(err_info):
# 将错误信息展示在页面上
return render_template("err_info.html", err_info=err_info)
# 出错时走错误页面加载流程Biz.login_failed_handler(err_info)
@app.errorhandler(Exception)
def auth_error_handler(ex):
return Biz.login_failed_handler(ex)
# 默认的主页路径
@app.route("/", methods=["GET"])
def get_home():
# 打开本网页应用会执行的第一个函数
# 如果session当中没有存储user info,则走免登业务流程Biz.login_handler()
if USER_INFO_KEY not in session:
logging.info("need to get user information")
return Biz.login_handler()
else:
# 如果session中已经有user info,则直接走主页加载流程Biz.home_handler()
logging.info("already have user information")
return Biz.home_handler()
@app.route("/callback", methods=["GET"])
def callback():
# 获取 user info
# 拿到前端传来的临时授权码 Code
code = request.args.get("code")
# 先获取 user_access_token
auth.authorize_user_access_token(code)
# 再获取 user info
user_info = auth.get_user_info()
# 将 user info 存入 session
session[USER_INFO_KEY] = user_info
return jsonify(user_info)
@app.route("/get_appid", methods=["GET"])
def get_appid():
# 获取 appid
# 为了安全,app_id不应对外泄露,尤其不应在前端明文书写,因此此处从服务端传递过去
return jsonify(
{
"appid": APP_ID
}
)
if __name__ == "__main__":
# 以debug模式运行本网页应用
# debug模式能检测服务端模块的代码变化,如果有修改会自动重启服务
app.run(host="0.0.0.0", port=3000, debug=True)
以上,免登流程完成。