全网首篇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官方文档为英文文档,以及对接过程中遇到许多问题,出一篇详解文章,如果有遇到问题的或者写错的地方可以评论区指正。

相关推荐
Rverdoser42 分钟前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse1 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴1 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811922 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌4 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫4 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai
YMWM_4 小时前
第一章 Go语言简介
开发语言·后端·golang
码蜂窝编程官方4 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
hummhumm4 小时前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊5 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程