前言:
Figma在近几年可以说是非常火的在线原型和UI设计工具,有不少设计人员称之为设计神器。我将会从以下两个问题来开始切入
为什么要对接Figma,对接后有什么用?
官方给出的答案是:Figma API支持读访问和与Figma文件的交互。这使您能够查看和提取任何对象或层,以及它们的属性,因此您可以将它们渲染为Figma之外的图像。然后,您可以展示您的设计,将它们连接到其他应用程序,或者使用它们扩展您的愿景。该API的未来版本将围绕文件解锁更强大的功能。
本文章会详细描述从0到1对接Figma的详细过程,剖析开发过程中遇到的相关问题及需要改进的地方
准备工作:
FIGMA官网:www.figma.com/developers/...
首先需要说明一下Figma授权支持两种方式:access tokens 和 OAuth2 ,access tokens方式对于三方系统的用户使用起来较为复杂,需要用户自行登录官网获取令牌且只有24小时有效期,本文只对接OAuth2的方式。
- 技术栈主要采用springBoot+redisson+okhttp3实现
- 对接之前需要注册应用与对接系统关联,地址:My apps
- 点击Create a new app 填写应用信息(重点:注意Callbacks的地址是授权成功后调用的地址)
- 保存后会得到一个 client ID 和 client 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回调
在此需要做的就是分三步
-
根据回调地址中的参数校验第一步生成地址中存储在redis中的用户
回调中会有两个重要参数:code和state,state为第一步中生成的唯一标识,code为交换访问令牌的验证代码,也就是调用token接口中的一个参数
-
使用post方式调用获取token的接口
-
请求头需要设置Basic Auth认证,以下为okhttp的示例
javapublic 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,用户刷新授权所用,有失效时间
-
保存用户授权信息 授权成功所返回的四个参数建议与自己系统对应的用户都保存起来,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在这方面做的缺点。
-
刷新授权请求地址:www.figma.com/api/oauth/r... 使用post方式请求
-
刷新授权的参数:
- client_id:应用ID
- client_secret:应用密钥
- refresh_token:刷新token
-
响应参数:
- 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官方文档为英文文档,以及对接过程中遇到许多问题,出一篇详解文章,如果有遇到问题的或者写错的地方可以评论区指正。