具体开发流程在飞书官方文档都有,大家可以参考这个链接:开发文档 - 飞书开放平台飞书开发文档中包含丰富多样的开发指南、教程和示例,让开发者获得愉悦、高效的应用开发体验。https://open.feishu.cn/document/uYjL24iN/uITO4IjLykDOy4iM5gjM
看完文档大家对开发流程都有了初步了解,鉴权就是整个流程中最关键的,看明白这张图你离成功就不远了。
其实后端要做的就是根据前端传来的需要鉴权的url,结合自己的App ID 和App Secret分别去请求得到对应access_token,ticket,最终生成signature返回给前端。请求的参数和方式在官方文档都有详细说明,这里不多赘述。
后端的部分代码:
/**
* 使用 App ID 和 App Secret 获取 tenant_access_token;使用 tenant_access_token 获取 jsapi_ticket;
* 使用jsapi_ticket、随机字符串、当前时间戳、当前鉴权的网页URL 生成签名signature
*/
@RestController
@Slf4j
public class AuthController {
@Autowired
protected ObjectMapper mapper;
@Autowired
HttpRequestExecutor executor;
private static String TENANT_ACCESS_TOKEN_URI="https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
private static String JSAPI_TICKET_URI = "https://open.feishu.cn/open-apis/jssdk/ticket/get";
private static String APP_ID ="cli_a49d44672462500d";
private static String APP_SECRET="NiEZR2I5b2Jjr4brLKwmWgFugrIUNYQW";
private static String NONCE_STR="13oEviLbrTo458A3NjrOwS70oTOXVOAm";
private static String APP_ACCESS_TOKEN_URI="https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal";
private static String USER_ACCESS_TOKEN_URI="https://open.feishu.cn/open-apis/authen/v1/access_token";
private static String USER_INFO_URI="https://open.feishu.cn/open-apis/authen/v1/user_info";
@GetMapping("/get_config_parameters")
public Map get_config_parameters(@RequestParam(required = true)String url) throws JsonProcessingException {
//接入方前端传来的需要鉴权的网页url
// 初始化Auth类时获取的jsapi_ticket
String ticket = get_ticket();
// 当前时间戳,毫秒级
String timestamp = String.valueOf(System.currentTimeMillis());
log.info("timestamp:{}",timestamp);
//jsapi_ticket={}&noncestr={}×tamp={}&url={}
String verify_str =String.format("jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s",ticket,NONCE_STR,timestamp,url);
log.info("verify_str:{}",verify_str);
//对字符串做sha1加密,得到签名signature
Digester md5 = new Digester(DigestAlgorithm.SHA1);
String signature = md5.digestHex(verify_str);
log.info("signature:{}",signature);
Map<String,String> map = new HashMap();
map.put("appid",APP_ID);
map.put("signature",signature);
map.put("noncestr",NONCE_STR);
map.put("timestamp",timestamp);
map.put("url",url);
return map;
}
@GetMapping("/callback")
public Map callback(@RequestParam(required = true)String code) throws JsonProcessingException {
log.info("前端传回来的code:{}",code);
UserInfoEntity userInfo = get_user_info(code);
log.info("用户信息获取成功!用户信息:{}",userInfo);
Map<String,String> map = new HashMap();
map.put("name", userInfo.getName());
map.put("en_name", userInfo.getEn_name());
map.put("avatar_url", userInfo.getAvatar_url());
map.put("avatar_thumb", userInfo.getAvatar_thumb());
map.put("avatar_middle", userInfo.getAvatar_middle());
map.put("avatar_big", userInfo.getAvatar_big());
map.put("open_id", userInfo.getOpen_id());
map.put("union_id", userInfo.getUnion_id());
map.put("email", userInfo.getEmail());
map.put("enterprise_email", userInfo.getEnterprise_email());
map.put("user_id", userInfo.getUser_id());
map.put("mobile", userInfo.getMobile());
map.put("tenant_key", userInfo.getTenant_key());
return map;
}
public String get_ticket() throws JsonProcessingException {
String access_token = getTenant_access_token();
BasicHeader access_tokenHeader =new BasicHeader("Authorization","Bearer "+access_token);
String str = executor.postForObjectAsString(JSAPI_TICKET_URI,null,access_tokenHeader);
log.info("get_ticket:{}",str);
//{"code":0,"data":{"expire_in":7200,"ticket":"g1043lbL6RLVEDXVKY2GCSAG2GUFJQR3EWQUSEOC"},"msg":"ok"}
TicketVo ticketVo = mapper.readValue(str, TicketVo.class);
log.info("ticketVo:{}",ticketVo);
return ticketVo.getData().getTicket();
}
public String getTenant_access_token() throws JsonProcessingException {
TokenRequestEntity requestEntity = new TokenRequestEntity();
requestEntity.setApp_id(APP_ID);
requestEntity.setApp_secret(APP_SECRET);
log.info("access_token_param:{}",requestEntity);
String res = executor.postForObjectAsString2(TENANT_ACCESS_TOKEN_URI,requestEntity);
//2023-03-21 11:36:36.109 INFO 20724 --- [io-3000-exec-10] com.taikang.controller.AuthController : access_token_result:{"code":0,"expire":7200,"msg":"ok","tenant_access_token":"t-g1043lbAZDEFM2F7UTYARU2U5JK2ODZ7LIAQ6ZWA"}
log.info("access_token_result:{}",res);
AccessTokenResultVo accessTokenResultVo= mapper.readValue(res, AccessTokenResultVo.class);
log.info("accessTokenResultVo:{}",accessTokenResultVo);
return accessTokenResultVo.getTenant_access_token();
}
public String getApp_access_token() throws JsonProcessingException {
TokenRequestEntity requestEntity = new TokenRequestEntity();
requestEntity.setApp_id(APP_ID);
requestEntity.setApp_secret(APP_SECRET);
log.info("access_token_param:{}",requestEntity);
String res = executor.postForObjectAsString2(APP_ACCESS_TOKEN_URI,requestEntity);
log.info("access_token_result:{}",res);
AppAccessTokenResultVo appAccessTokenResultVo= mapper.readValue(res, AppAccessTokenResultVo.class);
log.info("accessTokenResultVo:{}",appAccessTokenResultVo);
return appAccessTokenResultVo.getApp_access_token();
}
public UserInfoEntity get_user_info(String code) throws JsonProcessingException {
String app_access_token = getApp_access_token();
log.info("app_access_token:{}",app_access_token);
BasicHeader access_tokenHeader =new BasicHeader("Authorization","Bearer "+app_access_token);
UserAccessTokenRequestEntity userAccessTokenRequestEntity = new UserAccessTokenRequestEntity();
String grant_type = "authorization_code";
userAccessTokenRequestEntity.setCode(code);
userAccessTokenRequestEntity.setGrant_type(grant_type);
String str = executor.postForObjectAsString(USER_ACCESS_TOKEN_URI,userAccessTokenRequestEntity,access_tokenHeader);
UserAccessTokenResponseEntity userAccessTokenResponse = mapper.readValue(str, UserAccessTokenResponseEntity.class);
log.info("userAccessTokenResponse:{}",userAccessTokenResponse);
String user_access_token = userAccessTokenResponse.getData().getAccess_token();
log.info("user_access_token:{}",user_access_token);
BasicHeader user_access_tokenHeader =new BasicHeader("Authorization","Bearer "+user_access_token);
UserInfoResponseEntity userInfoResponse = executor.getForObject(USER_INFO_URI,UserInfoResponseEntity.class,user_access_tokenHeader);
return userInfoResponse.getData();
}
}
前端的部分代码:
index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>网页应用鉴权</title>
<link rel="stylesheet" href="/public/index.css" />
<script src="/js/jquery.js"></script>
<!-- 引入 JSSDK -->
<!-- JS 文件版本在升级功能时地址会变化,如有需要(比如使用新增的 API),请重新引用「网页应用开发指南」中的JSSDK链接,确保你当前使用的JSSDK版本是最新的。-->
<script
type="text/javascript"
src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.19.js"
></script>
<!-- 在页面上添加VConsole方便调试-->
<script src="/js/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
</head>
<body>
<div>
<div class="img">
<!-- 头像 -->
<div id="img_div" class="img_div"></div>
<span class="text_hello">Hello FEISHU</span>
<!-- 名称 -->
<div id="hello_text_name" class="text_hello_name"></div>
<!-- 欢迎语 -->
<div id="hello_text_welcome" class="text_hello_welcome"></div>
</div>
<!-- 飞书icon -->
<div class="icon"><img src="public/svg/icon.svg" /></div>
<!-- <div class="icon"><img width="80%" src="https://sf3-cn.feishucdn.com/obj/open-platform-opendoc/33e4ae2ff215314046c51ee1d3008d89_p1QpEy0jkK.png"/></div>-->
</div>
<script src="/public/index.js"></script>
<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>
index.js文件
let lang = window.navigator.language;
$("document").ready(apiAuth());
function apiAuth() {
console.log("start apiAuth");
if (!window.h5sdk) {
console.log("invalid h5sdk");
alert("please open in feishu");
return;
}
// 调用config接口的当前网页url
const UR = window.location.href.split('?')[0].split('#')[0];
console.log("重定向url为:", UR);
const url = encodeURIComponent(location.href.split("#")[0]);
console.log("接入方前端将需要鉴权的url发给接入方服务端,url为:", url);
// 向接入方服务端发起请求,获取鉴权参数(appId、timestamp、nonceStr、signature)
fetch(`/get_config_parameters?url=${url}`)
.then((response) =>
response.json().then((res) => {
console.log(
"接入方服务端返回给接入方前端的结果(前端调用config接口的所需参数):", res
);
console.log( "1");
// 通过error接口处理API验证失败后的回调
window.h5sdk.error((err) => {
throw ("h5sdk error:", JSON.stringify(err));
});
console.log( "2");
// 调用config接口进行鉴权
window.h5sdk.config({
appId: res.appid,
timestamp: res.timestamp,
nonceStr: res.noncestr,
signature: res.signature,
url:res.url,
jsApiList: [],
//鉴权成功回调
onSuccess: (res) => {
console.log( "3");
console.log(`config success: ${JSON.stringify(res)}`);
},
//鉴权失败回调
onFail: (err) => {
console.log( "42");
throw `config failed: ${JSON.stringify(err)}`;
}
});
console.log( "4");
// 完成鉴权后,便可在 window.h5sdk.ready 里调用 JSAPI
window.h5sdk.ready(() => {
// 调用 JSAPI tt.requestAuthCode 获取 authorization code
tt.requestAuthCode({
appId: res.appid,
// 获取成功后的回调
success(res) {
console.log("getAuthCode succeed");
console.log("res.code",res.code);
//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");
console.log("获取到的userInfo:",res2);
// 示例 Demo 中单独定义的函数 showUser,用于将用户信息展示在前端页面上
showUser(res2);}
)
).catch(function (e) {console.error(e)})
},
// 获取失败后的回调
fail(err) {
console.log(`getAuthCode failed, err:`, JSON.stringify(err));
}
})
// 调用 showToast API 弹出全局提示框,详细文档参见https://open.feishu.cn/document/uAjLw4CM/uYjL24iN/block/api/showtoast
tt.showToast({
title: "鉴权成功",
icon: "success",
duration: 3000,
success(res) {
console.log("showToast 调用成功", res.errMsg);
},
fail(res) {
console.log("showToast 调用失败", res.errMsg);
},
complete(res) {
console.log("showToast 调用结束", res.errMsg);
},
});
});
})
)
.catch(function (e) {
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");
}
我在进行调试的时候,前端提示了上图这个错误,最后才发现JSSDK虽然引入了,但是注入失败,但考虑到官方的demo出错的概率很小,我又去官方的开发文档里找,发现了下面这句话。
于是我在飞书应用里重新尝试打开创建的应用,鉴权成功!
想要直接体验效果的小伙伴可以直接下载飞书官方的demo
第三方网页应用对接飞书demo(python版):开发文档 - 飞书开放平台