2团
Published on 2025-03-19 / 18 Visits
0
0

基于飞书网页登录流程解释OAuth 2及JustAuth集成实践

1. 前言

在前期研究 yudao-cloud 代码时,对其中微信小程序的登录流程理解不够深入。然而,在后续项目中涉及 OAuth 2.0 对接需求时,通过实际操作与实践,相关流程逐渐变得清晰。因此,特在此进行详细记录,以便后续查阅与参考。

2. OAuth 2网页端登录流程

image-qxre.png

此处,借用飞书的网页应用登录流程图进行展示,网页应用登录流程流程可分为四部分,具体如下:

  • 获取登录授权码code;

  • 根据code获取access_token;

  • 根据access_token获取用户身份信息;

  • 刷新access_token(可选步骤,本文不做详细解释)。

2.1 获取登录授权码code

当用户访问网页应用并需要进行飞书授权时,系统会将网页重定向至飞书的授权页面。用户在授权页面完成授权操作后,浏览器将携带授权码code)跳转至网页应用在飞书平台配置的回调地址redirect_uri)。

2.1.1 授权码请求

请求URL示例:https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=cli_a5d611352af9d00b&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Foauth%2Fcallback&scope=bitable:app:readonly%20contact:contact&state=RANDOMSTRING

URL中关键参数讲解:

  • redirect_uri :

    • 授权应用重定向地址,注意拼接至URL时需要进行URL编码;

    • 此地址一般实现为前端空白页,由前端空白页将code参数传递至网页应用后端。

  • client_id:网页应用在飞书登记的App ID;

  • state

    • 用来维护请求和回调之间状态的附加字符串,在授权完成回调时会原样回传此参数,应用可以根据此字符串来判断上下文关系;

    • 该参数也可以用以防止 CSRF 攻击,请务必校验 state 参数前后是否一致;

    • 实现上可以为随机字符串;

    • 一般上为非必填字段,若网页应用未生成,则授权应用回调时会忽略此字段。

2.2.1 授权码响应

授权成功响应示例:https://example.com/api/oauth/callback?code=2Wd5g337vo5BZXUz-3W5KECsWUmIzJ_FJ1eFD59fD1AJIibIZljTu3OLK-HP_UI1&state=RANDOMSTRING

当用户同意授权后,浏览器将携带授权码code重定向到发起授权时给定的redirect_uri (若网页应用发起授权请求时携带state,则重定向地址也将携带state)。

2.2 获取access_token

OAuth 令牌接口,可用于获取 user_access_token 以及 refresh_token(部分授权应用可能未实现refresh_token)。

  • user_access_token 为用户访问凭证,使用该凭证可以以用户身份调用 OpenAPI。

  • refresh_token 为刷新凭证,可以用来获取新的 user_access_token

2.2.1 access_token请求

请求参数示例:

{
    "grant_type": "authorization_code",
    "client_id": "cli_a5ca35a685b0x26e",
    "client_secret": "baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy",
    "code": "a61hb967bd094dge949h79bbexd16dfe",
    "redirect_uri": "https://example.com/api/oauth/callback",
    "code_verifier": "TxYmzM4PHLBlqm5NtnCmwxMH8mFlRWl_ipie3O0aVzo"
}

参数解释:

  • grant_type :授权类型,当前场景固定为authorization_code

  • client_id同2.1.1章节的解释;

  • client_secret :网页应用分配的secret,授权应用使用client_id以及client_secret综合验证网页应用身份;

  • redirect_uri :同2.1.1章节的解释。

2.2.2 access_token响应

成功响应示例:

{
    "code": 0,
    "access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg",
    "expires_in": 7200, // 非固定值,请务必根据响应体中返回的实际值来确定 access_token 的有效期
    "refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA",
    "refresh_token_expires_in": 604800, // 非固定值,请务必根据响应体中返回的实际值来确定 refresh_token 的有效期
    "scope": "auth:user.id:read offline_access task:task:read user_profile",
    "token_type": "Bearer"
}

参数解释:

  • expires_inaccess_token失效时间,默认单位为秒;

  • refresh_token用于刷新access_token;

2.3 根据access_token获取用户信息

网页应用后端服务,需要根据2.2章节获取的access_token(一般存储至header中),访问授权应用获取用户的信息。

以下是飞书返回的个人信息示例(一般使用open_id作为用户的身份唯一标识):

{
    "code": 0,
    "msg": "success",
    "data": {
        "name": "zhangsan",
        "en_name": "zhangsan",
        "avatar_url": "www.feishu.cn/avatar/icon",
        "avatar_thumb": "www.feishu.cn/avatar/icon_thumb",
        "avatar_middle": "www.feishu.cn/avatar/icon_middle",
        "avatar_big": "www.feishu.cn/avatar/icon_big",
        "open_id": "ou-caecc734c2e3328a62489fe0648c4b98779515d3",
        "union_id": "on-d89jhsdhjsajkda7828enjdj328ydhhw3u43yjhdj",
        "email": "zhangsan@feishu.cn",
        "enterprise_email": "demo@mail.com",
        "user_id": "5d9bdxxx",
        "mobile": "+86130002883xx",
        "tenant_key": "736588c92lxf175d",
		"employee_no": "111222333"
    }
}

3. JuatAuth完成飞书网页应用登录

JustAuth是一款开箱即用的开源组件,能够高效整合第三方登录功能。在yudao-cloud项目中,它被广泛应用于众多第三方登录的集成工作。鉴于我们之前已经详细介绍了飞书网页应用的登录流程,接下来我们将深入探讨如何借助JustAuth实现飞书登录流程的具体操作。

3.1 创建AuthRequest

创建AuthRequest,根据在飞书平台的注册信息,完成相应参数的填写。

AuthRequest authRequest = new AuthFeishuRequest(AuthConfig.builder()
                .clientId("App ID")
                .clientSecret("App Secret")
                .redirectUri("重定向 URL")
                .build());

3.2 生成飞书的授权地址

String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());

这个链接我们可以直接后台重定向跳转,也可以返回到前端后,前端控制跳转。

前端控制的好处就是,可以将第三方的授权页嵌入到iframe中,适配网站设计。

生成飞书授权地址的逻辑比较简单,具体如下所示:

    @Override
    public String authorize(String state) {
        return UrlBuilder.fromBaseUrl(source.authorize())
                .queryParam("app_id", config.getClientId())
                .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
                .queryParam("state", getRealState(state))
                .build();
    }

3.3 完整流程示意

@RestController
@RequestMapping("/oauth")
public class RestAuthController {

    @RequestMapping("/render")
    public void renderAuth(HttpServletResponse response) throws IOException {
        AuthRequest authRequest = getAuthRequest();
        response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
    }

    @RequestMapping("/callback")
    public Object login(AuthCallback callback) {
        AuthRequest authRequest = getAuthRequest();
        return authRequest.login(callback);
    }

    private AuthRequest getAuthRequest() {
        return new AuthFeishuRequest(AuthConfig.builder()
                .clientId("App ID")
                .clientSecret("App Secret")
                .redirectUri("重定向 URL")
                .build());
    }
}

完整版代码如上所示,通过以上代码即可完成飞书网页应用的登录流程。

JustAuth可以帮助我们省去较多的样板代码,此外还可以帮我们托管access_token等,减少开发工作量。

4. JustAuth完成小程序登录

小程序的登录流程与OAuth 2.0流程在原理上具有相似性,但为了适应小程序的特定环境和开发规范,需要进行一些针对性的适配工作。主要区别点在于:

  • 微信小程序的授权码code是通过小程序中的wx.login函数获取的,因此无需配置redirect_uri

  • 微信小程序授权码是小程序中获取的,因此不需要校验state

4.1 创建AuthRequest

AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
                .clientId("xx")
                .clientSecret("xx")
                .ignoreCheckRedirectUri(true)
                .ignoreCheckState(true)
                .build());

根据上文的描述,微信小程序的AuthRequest创建如上图所示(可与3.1章节进行比较)。

4.2 生成微信小程序的授权地址

小程序平台的授权登录不需要回调地址,不需要调用authRequest.authorize(AuthStateUtils.createState())方法,此步略去。

4.3 完整流程示意

@RestController
@RequestMapping("/oauth")
public class RestAuthController {

    @RequestMapping("/callback")
    public Object login(AuthCallback callback) {
        AuthRequest authRequest = getAuthRequest();
        return authRequest.login(callback);
    }

    private AuthRequest getAuthRequest() {
        return new AuthWechatMiniProgramRequest(AuthConfig.builder()
                .clientId("xx")
                .clientSecret("xx")
                .ignoreCheckRedirectUri(true)
                .ignoreCheckState(true)
                .build());
    }
}

5 总结

不同公司,乃至同一公司内部的各个部门,在实现OAuth 2.0流程时往往存在差异,并未完全严格按照标准规范执行。

因此,我们唯有深入熟悉并理解这些流程的细节与特点,才能更精准、高效地编写相应的对接代码,确保系统的顺利集成与运行。


Comment