前言
最近接收到了一个系统接入飞书的需求,大概就是当前系统只有账户密码登录,需要接入飞书扫码登录,另外还可以在飞书里面点击应用直接实现系统用户免登录。解析一下这个需求就是在浏览器端使用飞书扫码授权完成登录验证,同时需要提供飞书内部应用直接免登功能。哈哈,我调研了一些时间后将飞书网页应用登录与免登录接入了我们系统。现在就主要的接入点分享备注,以备二次查阅。
技术积累
飞书网页应用登录
网页应用登录是基于 OAuth 2.0 标准协议实现的,通过飞书账号授权登录第三方应用的能力。
在使用网页应用登录能力之前,首先在 开放平台开发者后台(https://open.feishu.cn/app/) 创建一个应用,并获得应用的 AppID 和 AppSecret。
接下来,需要选中【安全设置】一栏,并将接收授权码的页面地址添加到右侧的【重定向URL】。
整体流程
接口文档:获取登录授权码code
https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code
第二步:获取 user_access_token
接口文档:获取 user_access_token
https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/oidc-access_token/create
第三步:获取用户身份信息
接口文档:获取用户身份信息
https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get
第四步:刷新 user_access_token
接口文档:刷新 user_access_token
https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/oidc-refresh_access_token/create
飞书网页应用免登
在开放平台(https://open.feishu.cn/app/)创建好自建应用后,即可在本地开发相应的 Web 项目。如果你希望应用在飞书客户端内被访问时,可以获取客户端登录用户的信息,实现免登访问应用,则可以为应用配置免登流程。
在使用网页应用免登录之前,需要在自建应用新增网页应用功能,并配置 桌面端主页 的URL。
接下来,需要选中【安全设置】一栏,并将接收授权码的页面地址添加到右侧的【重定向URL】。
如果用户已登录飞书客户端,则可以直接访问客户端内的网页应用,无需二次登录。在实际开发应用的过程中,可以通过如下流程图获取已登录用户的信息。
流程说明:
上图中的 App ID 和 App Secret 为整个流程仅需的输入信息。
App ID 和 App Secret 参数位于开发者后台中该应用的详情页面凭证与基础信息。
上图中飞书客户端和接入方服务端,与认证中心的数据交换通过 HTTP 请求完成。
涉及数据交换的步骤序号:12、45、89、1011。
上图中接入方前端与飞书客户端的数据交换(步骤序号:3~6)通过 JSAPI 调用完成。
上图中接入方前端与接入方服务端的数据交换(步骤序号:7、12)在你的网页应用框架内自行完成。
JustAuth三方授权中间件
JustAuth,是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录 SDK,让登录变得So easy!
JustAuth 集成了诸如:Github、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow、飞书等国内外数十家第三方平台。
实战演示
我们在DEMO中直接使用JustAuth三方中间件集成,简单的代码即可完成飞书接入流程。
我们后端采用springboot,由于飞书免登录需要使用到前端我们使用thymeleaf来模拟前端即可。
本DEMO不可直接使用在生产环境,只是简单将飞书授权接入系统,并未将系统用户打通。如果需要运用于生产环境,可以考虑增加飞书用户表、系统用户与飞书用户关系表,并在完成飞书授权后增加本地系统TOKEN功能,以完成飞书验证授权登录全流程。
后端代码
引入JustAuth、thymeleaf 和 Redis依赖
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 对象池,使用redis时必须引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
如果okhttp报错异常,需要引入okhttp依赖
<!-- https://mvnrepository.com/artifact/org.web3j/core -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.3.1</version>
</dependency>
application增加配置
spring:
redis:
host: 127.0.0.1
port: 6379
# 连接超时时间(记得添加单位,Duration)
timeout: 10000ms
# Redis默认情况下有16个分片,这里配置具体使用的分片
database: 9
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
thymeleaf:
mode: HTML
encoding: UTF-8
content-type: text/html
cache: false
prefix: classpath:/templates/
#飞书授权
justauth:
enabled: true
type:
FEISHU:
client-id: cli_a6e150ae217b500e
client-secret: BycuJEvauWMaOtO8b0rYcdVSigQfhZDf
redirect-uri: http://45s7p8.natappfree.cc/oauth/feishu/callback
ignore-check-state: true
cache:
type: redis
# 缓存前缀,目前只对redis缓存生效,默认 JUSTAUTH::STATE::
prefix: 'SOCIAL::STATE::'
# 超时时长,目前只对redis缓存生效,默认3分钟
timeout: 1h
后端接口
import com.alibaba.fastjson.JSONObject;
import com.xkcoding.justauth.AuthRequestFactory;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* AuthController
* @author senfel
* @version 1.0
* @date 2024/7/16 10:11
*/
@Slf4j
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Resource
private AuthRequestFactory factory;
@GetMapping
public List<String> list() {
return factory.oauthList();
}
/**
* 跳转授权页面
* http://45s7p8.natappfree.cc/oauth/login/feishu
* @param type
* @param response
* @author senfel
* @date 2024/7/16 10:24
* @return void
*/
@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}
/**
* 回调
* http://45s7p8.natappfree.cc/oauth/feishu/callback?code=83fs2ab42c1e4d278ffcafc8ee2112ff&state=23ca3e7e9efbca9744cd7924de2a5f77
* @param type
* @param callback
* @author senfel
* @date 2024/7/16 10:25
* @return me.zhyd.oauth.model.AuthResponse
*/
@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
log.info("【response】= {}", JSONObject.toJSONString(response));
return response;
}
/**
* 获取飞书网页授权页面
* http://45s7p8.natappfree.cc/oauth/feishu/auth
* @author senfel
* @date 2024/7/16 14:03
* @return org.springframework.web.servlet.ModelAndView
*/
@GetMapping("/{type}/auth")
public ModelAndView auth(){
ModelAndView auth = new ModelAndView("auth");
return auth;
}
}
thymeleaf页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>网页应用鉴权</title>
<link rel="stylesheet" href="/public/index.css" />
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.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.16.js"></script>
<!-- 在页面上添加VConsole方便调试-->
<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
<!--调试js-->
<script src='https://sf1-scmcdn-cn.feishucdn.com/obj/feishu-static/op/fe/devtools_frontend/remote-debug-0.0.1-alpha.6.js'></script>
</head>
<body style="background: #fff;">
<div>飞书网页免登录</div>
<div id="status" style="color: #000;font-size: 38px;"></div>
<div id="response" style="color: #000;font-size: 18px;"></div>
</body>
<script>
var divSta = document.getElementById("status");
var divRes = document.getElementById("response");
divSta.innerText = "免登录中......";
var vConsole = new window.VConsole();
tt.requestAuthCode({
appId: 'cli_a6e150ae217b500e',
// 获取成功后的回调
success(res) {
console.log(res,"--------------getAuthCode succeed");
$.get(`http://45s7p8.natappfree.cc/oauth/feishu/callback?code=${res.code}`,function(data,status){
console.log(data,status,'==========')
divSta.innerText = "免登录成功";
divRes.innerText = JSON.stringify(data);
});
},
// 获取失败后的回调
fail(err) {
console.log(`getAuthCode failed, err:`, JSON.stringify(err));
}
})
</script>
</html>
飞书网页应用授权
阅读上面的技术积累自行完成自建应用的创建,获取到AppID 和 AppSecret。
配置扫码授权回调地址白名单:
浏览器访问后端的授权接口 http://45s7p8.natappfree.cc/oauth/login/feishu
会直接生成飞书的跳转链接并重定向到授权页面:
如果此时我们pc没有登录飞书,就是一个二维码页面,当然我们也可以选择其他账号调用二维码页面
然后我们用飞书扫码授权账号登录后,也会跳转调上一个图片选择账号授权。
点击授权后飞书会回调我们后端配置的路径,并且路径中带授权码和状态值,我们拿到授权码后进行后续授权流程以获取当前授权用户信息以完成授权流程。
拿到授权用户信息后,我们就可以自己做一些东西了。比如飞书用户入库、绑定系统用户、飞书用户token缓存等等。然后后端根据需要生成自己系统的token并进入系统首页,这样就打通了飞书授权流程了。
网页应用免登录
飞书应用配置网页应用
给我们的自建应用新增一个网页应用功能
配置网页应用选择默认的飞书内部标签页打开
安全设置重定向URL配置
调试免登录
点击去调试案例进入调试配置页面
将调试js放入我们的前端代码
然后点击生成并打开调试地址
确认并打开调试页面
然后飞书就会调用我们配置的回调地址打开页面
在这个授权页面中我们会拿到飞书的临时授权码
然后我们会发送请求调用后端获取到当前用户的token,拿到token了我们就可以认为完成了授权流程。
飞书工作台进入应用
去除auth页面中的调试js,测试企业默认就发布,不用额外发布了
从飞书应用进入免登录
至此,我们已经完成飞书网页应用授权与免登录主体流程。如果需要接入自己的系统,只需要将后端代码贴入,并增加飞书用户与系统用户绑定、生成内部系统token功能即可完成飞书验证。当然,如果需要接入飞书免登录可以将html放入前端项目并进行适当修改即可。