全网首篇Figma授权详解

前言:

Figma在近几年可以说是非常火的在线原型和UI设计工具,有不少设计人员称之为设计神器。我将会从以下两个问题来开始切入

为什么要对接Figma,对接后有什么用?

官方给出的答案是:Figma API支持读访问和与Figma文件的交互。这使您能够查看和提取任何对象或层,以及它们的属性,因此您可以将它们渲染为Figma之外的图像。然后,您可以展示您的设计,将它们连接到其他应用程序,或者使用它们扩展您的愿景。该API的未来版本将围绕文件解锁更强大的功能。

本文章会详细描述从0到1对接Figma的详细过程,剖析开发过程中遇到的相关问题及需要改进的地方

准备工作:

FIGMA官网:www.figma.com/developers/...

首先需要说明一下Figma授权支持两种方式:access tokensOAuth2 ,access tokens方式对于三方系统的用户使用起来较为复杂,需要用户自行登录官网获取令牌且只有24小时有效期,本文只对接OAuth2的方式。

  1. 技术栈主要采用springBoot+redisson+okhttp3实现
  2. 对接之前需要注册应用与对接系统关联,地址:My apps
  3. 点击Create a new app 填写应用信息(重点:注意Callbacks的地址是授权成功后调用的地址
  4. 保存后会得到一个 client IDclient secret ,需要注意的是client secret只会显示给你一次!马上复制,存到安全的地方

对接步骤

1. 系统生成授权地址

官方文档地址:www.figma.com/developers/...

  • 校验当前登录用户是否已授权,检查授权方法中的expiresIn为授权成功的回调中返回的后面会讲到
  • 拼接授权地址
    • client_id:已创建的应用ID
    • redirect_uri:重定向地址必须和创建应用时设置的Callbacks地址一致否则会提示地址参数错误
    • scope:作用域,也就是授权成功后哪些接口可以成功访问,多个作用域使用空格分隔或逗号分隔,全部作用域地址:www.figma.com/developers/...
    • state:该参数官方推荐为随机生成的唯一值,可以用于区分授权成功后回调接口中的具体用户
    • response_type:由于figma暂时只支持OAuth 2的授权代码流此参数,可以固定设置为code
    • code_challenge:可选,figma官方强烈推荐即使用PKCE模式,此方式已经实际尝试过但是没走通,如果各位大佬实现了可以补充
  • 将生成的唯一code与用户ID绑定存储到redis中,并返回拼接后的授权地址

以下是具体代码实现:

java 复制代码
    /**
     * 系统服务地址,此处ip地址为公网地址,否则无法回调
     */
    @Value("${ip:http://127.168.0.1:8085/}")
    private String ipAddress;

    // figma地址
    private final String baseUrl = "https://www.figma.com";

    // 当前方法为具体实现
    public FigmaApiBo authorization(FigmaApiBo figmaApiBo) {
            // 校验当前用户是否授权
            if (getUserAuthorization(figmaApiBo.getUserId(), false) != null) {
                throw new BusinessException("1", "Authorization failure The current user has been authorized", "授权失败当前用户已授权过无需重复操作");
            }

            // 模拟获取缓存或字典中的figma系统配置
            FigmaBaseConfig figmaBaseConfig = getFigmaConfig();
            if (figmaBaseConfig == null) {
                throw new ExternalSystemException(Errors.SYSTEM_CONFIGURATION_ERROR);
            }

            // 生成授权地址
            String authorizationUrlStrBuilder = baseUrl +
                    "/oauth?client_id=" + figmaBaseConfig.getClientId() +
                    "&redirect_uri=" + ipAddress + "/api/web/figmaApi/authorizationCallBack" +
                    "&scope=" + FigmaScopesEnums.FILE_CONTENT.getScopes() +
                    "&state=" + IdUtil.fastSimpleUUID() +
                    "&response_type=code";

            // 信息保存至redis
            Map<String, Object> map = new HashMap<>();
            map.put("userId", figmaApiBo.getUserId().toString());
            map.put("scopes", FigmaScopesEnums.FILE_CONTENT.getScopes());
            boolean isSuccess = RedisUtils.hmset(FigmaConstant.REDIS_KEY_FIGMA_AUTHORIZATION_STATE + randomState, map, 7200L);
            if (!isSuccess) {
                log.error("授权保存code失败:::{}", figmaApiBo.toString());
                throw new BusinessException(Errors.SYSTEM_ERROR);
            }

            FigmaApiBo figmaApiData = new FigmaApiBo();
            figmaApiData.setAuthorizationUrl(authorizationUrlStrBuilder);
            figmaApiData.setUserId(figmaApiBo.getUserId());
            return figmaApiData;
        }

    private UserAuthorization getUserAuthorization(Long userId, Boolean verifyAuthorization) {
        UserAuthorizationBo userAuthorizationParam = new UserAuthorizationBo();
        userAuthorizationParam.setUserId(userId);
        userAuthorizationParam.setChannelType(WebApiChannelEnum.FIGMA.getCode());
        userAuthorizationParam.setAuthorizationSource(source);
        UserAuthorization userAuthorization = userAuthorizationDao.getUserAuthorizationOneByParam(userAuthorizationParam);

        Date currentTime = new Date();
        Date expiryTime = new Date(currentTime.getTime() - userAuthorization.getExpiresIn() * 1000);
        if (verifyAuthorization && currentTime.getTime() - expiryTime.getTime() > userAuthorization.getRefreshTime().getTime()) {
            throw new BusinessException("1", "当前账户授权已过期", "当前账户授权已过期");
        }

        return userAuthorizationDao.getUserAuthorizationOneByParam(userAuthorizationParam);
    }

2. 直接访问授权地址授权

重要提示:此URL必须通过用户的浏览器访问,而不能通过应用程序内嵌的webview访问

访问后会重定向授权页面,如果未登录Figma会是Figma登录页。

3. 同意授权,系统接收Figma回调

在此需要做的就是分三步

  1. 根据回调地址中的参数校验第一步生成地址中存储在redis中的用户

    回调中会有两个重要参数:code和state,state为第一步中生成的唯一标识,code为交换访问令牌的验证代码,也就是调用token接口中的一个参数

  2. 使用post方式调用获取token的接口

    • 接口地址为:www.figma.com/api/oauth/t...

    • 请求头需要设置Basic Auth认证,以下为okhttp的示例

      java 复制代码
      public C basicAuth(String username, String password) {
          byte[] authData = (username + ':' + password).getBytes(StandardCharsets.UTF_8);
          byte[] authBytes = Base64.getEncoder().encode(authData);
          String authStr = new String(authBytes, StandardCharsets.UTF_8);
          return addHeader("Authorization", "Basic " + authStr);
      }
    • 参数如下:

      • client_id:应用id
      • client_secret:应用密钥
      • redirect_uri:重定向地址,需要与第一步授权地址中的一致
      • code:上面成功授权回调中返回的code
      • grant_type:固定为:authorization_code即可
      • code_verifier:如果第一步中设置了PKCE模式必须提供用于生成代码质询的验证器 此方式已经实际尝试过但是没走通,如果各位大佬实现了可以补充
    • 响应参数:

      • user_id:授权用户与Figma应用的唯一ID
      • access_token:授权token,用于请求Figma接口所用
      • expires_in:过期时间
      • refresh_token:刷新授权token,用户刷新授权所用,有失效时间
  3. 保存用户授权信息 授权成功所返回的四个参数建议与自己系统对应的用户都保存起来,access_token可以用来刷新授权token,这也可以避免用户重复操作授权。重点:执行完当前实现后需要重定向到结果页面显示具体的结果

java 复制代码
/** 
 * 系统服务地址,此处ip地址为公网地址,否则无法回调 
 */ 
@Value("${ip:http://127.168.0.1:8085/}") 
private String ipAddress; 
// figma地址 
private final String baseUrl = "https://www.figma.com";

public WebResult authorizationCallBack(FigmaCallBackBo figmaCallBackBo) {
        //授权回调逻辑
        log.info("进入授权回调处理:{}", figmaCallBackBo.toString());

        FigmaBaseConfig figmaBaseConfig = getFigmaConfig();
        if (figmaBaseConfig == null) {
            log.info("暂未查询到figma配置");
            return WebResult.fail("1", "授权失败");
        }

        Map<Object, Object> map = RedisUtils.hmget(FigmaConstant.REDIS_KEY_FIGMA_AUTHORIZATION_STATE + figmaCallBackBo.getState());
        if (StringUtils.isEmpty(String.valueOf(map.get("userId")))) {
            log.info("暂未获取到用户授权信息");
            return WebResult.fail("1", "授权失败");
        }

        String authorizationCallBackUrl = ipAddress + "/api/web/figmaApi/authorizationCallBack";

        HttpResult httpResult = OkHttps.sync(baseUrl + "/api/oauth/token")
                .addBodyPara("client_id", figmaBaseConfig.getClientId())
                .addBodyPara("client_secret", figmaBaseConfig.getClientSecret())
                .addBodyPara("redirect_uri", authorizationCallBackUrl)
                .addBodyPara("code", figmaCallBackBo.getCode())
                .addBodyPara("grant_type", "authorization_code")
                .basicAuth(figmaBaseConfig.getClientId(), figmaBaseConfig.getClientSecret())
                .post();

        if (!HttpResult.State.RESPONSED.equals(httpResult.getState())) {
            log.error("请求响应失败");
            return WebResult.fail("1", "授权失败");
        }

        // 请求未成功响应
        if (200 != httpResult.getStatus()) {
            log.error("请求未成功响应");
            return WebResult.fail("1", "授权失败");
        }

        Long userId = Long.valueOf(String.valueOf(map.get("userId")));
        String scopes = String.valueOf(map.get("scopes"));

        FigmaToken figmaToken = httpResult.getBody().toBean(FigmaToken.class);
        log.info("授权成功响应:{}", figmaToken.toString());

        // 校验用户是否被绑定到其他账号,此处可以根据自己系统的字段和需求进行调整
        boolean isBind = userAuthorizationService.verifyUserAuthorizationBind(WebApiChannelEnum.FIGMA.getCode(), figmaToken.getUser_id(), userId);
        if (isBind) {
            log.error("授权失败,当前figma账户已被其他用户绑定");
            return WebResult.fail("1", "授权失败,当前figma账户已被其他用户绑定");
        }

        UserAuthorization userAuthorization = new UserAuthorization();
        userAuthorization.setUserId(userId);
        userAuthorization.setExtId(figmaToken.getUser_id());
        userAuthorization.setAccessToken(figmaToken.getAccess_token());
        userAuthorization.setRefreshToken(figmaToken.getRefresh_token());
        userAuthorization.setExpiresIn(figmaToken.getExpires_in());
        userAuthorization.setScopes(scopes);
        userAuthorization.setChannelType(WebApiChannelEnum.FIGMA.getCode());
        userAuthorization.setChannelId("");
        // 来源申请figma账号的主体()
        userAuthorization.setAuthorizationSource("G7");
        userAuthorization.setAuthorizationTime(new Date());
        boolean isSuccess = userAuthorizationDao.saveOrUpdateUserAuthorization(userAuthorization);
        
        // 保存成功删除redis中的信息,这里使用的是spring监听事件
        if (isSuccess) {
            RedisUtils.del(FigmaConstant.REDIS_KEY_FIGMA_AUTHORIZATION_STATE + key);
        }
        return WebResult.success(isSuccess);
    }
java 复制代码
@RequestMapping(value = "/authorizationCallBack", method = RequestMethod.GET)
public ModelAndView authorizationCallBack(FigmaCallBackReqForm figmaReqForm, ModelMap modelMap) {
    FigmaCallBackBo figmaCallBackBo = BeanUtil.copyProperties(figmaReqForm, FigmaCallBackBo.class);
    WebResult<?> webResult = figmaApiService.authorizationCallBack(figmaCallBackBo);
    modelMap.addAttribute("channelType", WebApiChannelEnum.FIGMA.getCode());
    modelMap.addAttribute("code", webResult.getCode());
    modelMap.addAttribute("data", webResult.getData());
    modelMap.addAttribute("msg", webResult.getMsg());
    return new ModelAndView("pub/web/callBackResult", modelMap);
}

刷新授权

默认情况下,OAuth令牌将在90天后过期,因此如果集成是长期存在的,则需要刷新存储的令牌。可以使用refresh_token来完成此操作。需要注意的是如果用户重复同意授权那首次授权的refresh_token就会失效,无法使用。也就是如果在系统中两个用户同时使用一个Figma账户授权首次授权成功的会失效,也就无法在授权前判断将要授权的Figma账户是否在自己的系统中已经授权过,如果对接过抖音授权的,在用户授权前还有一个token通常会返回唯一的用户ID,就可以和自己系统中的对比,个人认为这也是Figma在这方面做的缺点。

  1. 刷新授权请求地址:www.figma.com/api/oauth/r... 使用post方式请求

  2. 刷新授权的参数:

    • client_id:应用ID
    • client_secret:应用密钥
    • refresh_token:刷新token
  3. 响应参数:

    • expires_in:过期时间
    • access_token:授权token
java 复制代码
HttpResult httpResult = OkHttps.sync(baseUrl + "/api/oauth/refresh")
        .addBodyPara("client_id", figmaBaseConfig.getClientId())
        .addBodyPara("client_secret", figmaBaseConfig.getClientSecret())
        .addBodyPara("refresh_token", userAuthorization.getRefreshToken())
        .post();

具体API请求

在这里使用获取用户信息的接口举例,需要注意以上属于授权的相关接口与具体的业务接口API路径不相同

ini 复制代码
String apiUrl = "https://api.figma.com";
HttpResult httpResult = OkHttps.sync(apiUrl + "/v1/me")
        .bearerAuth(userAuthorization.getAccessToken())
        .get();

请求头需要设置Bearer Auth认证,下面为okhttp具体方法

java 复制代码
public C bearerAuth(String token) {
    return addHeader("Authorization", "Bearer " + token);
}

总结:

到此完整的授权及调用流程就结束了,由于Figma官方文档为英文文档,以及对接过程中遇到许多问题,出一篇详解文章,如果有遇到问题的或者写错的地方可以评论区指正。

相关推荐
云技纵横17 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户67570498850217 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan17 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户67570498850217 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia17 小时前
Geo Scene域名修改引起的一些问题
后端
用户2986985301417 小时前
Java 实现 Word 文档加密与权限解除
java·后端
vanuan18 小时前
给你的A2A-Agent加把锁-认证鉴权实战指南
后端
Yeats_Liao18 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构
未秃头的程序猿18 小时前
告别"if-else地狱"!Java 21模式匹配,代码优雅了10倍
java·后端·面试