调查问卷Type Form的集成

文章介绍了如何使用Typeform创建动态问卷并配置后端服务来接收和处理回调。内容涉及设置回调地址、验证签名(使用HMAC-SHA256算法)以及拦截和处理Typeform的请求。同时,文章还展示了SpringSecurity的配置以忽略特定URL并处理HTTP错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

Typeform是一家制作线上调查问卷的公司。

Muñoz 和 David Okuniev两人于2012年创作出一个更加动态、更具交互性的用户调查工具,每次只提一个问题,并且根据用户的回答为其呈现下一个问题,像和朋友间的对话一样,让用户在不知不觉中就完成了问卷。

Typeform将帮你获得有关产品和经验的反馈,创建和分享反馈、建立联系人表单,进行客户开发调查,结果将以交互式表格的形式快速发送到你的智能手机、平板电脑和台式电脑上。

Type Form 配置

1. 首先注册帐号,然后创建自己的调查问卷模版。 

 2.配置后端服务的回调地址和验签的密钥。

3. 根据需要可以配置隐藏的参数,隐藏参数可以通过回调接口传过来。 

 回调代码

1.开放Type Fome接口无需Token校验。

spring.security 配置不拦截的url.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...

  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/auth/**", "/actuator/health", "/satisfaction/callback");
  }

}

2.拦截Type Fome接口,校验签名。

注册Bean进行全局的url拦截。

  @Bean
  public TypeformFilter typeformContentCacheFilter() {
    return new TypeformFilter();
  }

拦截Type Form的url,然后进行 HmacSha256Signature 签名校验。

注意: filterChain.doFilter(servletRequestWrapper, httpServletResponse),由于HttpServletRequest去读一次后就会释放掉资源,所以需要把请求报文缓存起来。

@Slf4j
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class TypeformFilter extends OncePerRequestFilter {

  public static final String TYPEFORM_SATISFACTION_CALLBACKS = "/satisfaction/callback";

  @Value("${typeform.sha.key}")
  private String key;

  @Override
  public void destroy() {

  }

  @Override
  protected void doFilterInternal(@NonNull HttpServletRequest httpServletRequest,
      @NonNull HttpServletResponse httpServletResponse,
      @NonNull FilterChain filterChain) throws ServletException, IOException {
    CachedBodyHttpServletRequest servletRequestWrapper = new CachedBodyHttpServletRequest(
        httpServletRequest);
    String requestUri = servletRequestWrapper.getRequestURI();
    if (requestUri.equals(TYPEFORM_SATISFACTION_CALLBACKS)) {
      String signature = servletRequestWrapper.getHeader("Typeform-Signature");
      String payload = servletRequestWrapper.read();
      if (!validateHmacSha256Signature(signature, payload, key)) {
        httpError(httpServletResponse);
        return;
      }
    }
    filterChain.doFilter(servletRequestWrapper, httpServletResponse);
  }

  private static boolean validateHmacSha256Signature(String signature, String payload, String key) {
    return getHmacSha256(payload, key).equals(signature);
  }

  private static String getHmacSha256(String payload, String key) {
    return "sha256=" + HashUtil.hmacWithAlgorithm("HmacSHA256", payload, key);
  }

  public static void httpError(HttpServletResponse response) throws IOException {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    PrintWriter out = response.getWriter();
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    out.print("Invalid Request");
    out.flush();
    out.close();
  }

}

hmac算法的工具类,通过算法名称,请求报文,签名密钥获取加密后的报文。 

@Slf4j
public class HashUtil {

  public static String hmacWithAlgorithm(String algorithm, String payload, String key) {
    try {
      SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm);
      Mac mac = Mac.getInstance(algorithm);
      mac.init(secretKeySpec);
      return Base64.getEncoder().encodeToString((mac.doFinal(payload.getBytes())));
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
      log.error("hmacWithAlgorithm error", e);
      return null;
    }
  }
}

 CachedBodyHttpServletRequest 缓存了请求报文,以便后续拦截器过滤使用。

缓存代码:this.requestBody = StreamUtils.copyToByteArray(is);

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

  private final byte[] requestBody;

  public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
    super(request);
    try (InputStream is = request.getInputStream();) {
      this.requestBody = StreamUtils.copyToByteArray(is);
    }
  }

  @Override
  public ServletInputStream getInputStream() {
    return new ServletInputStreamWrapper(this.requestBody);
  }

  @Override
  public BufferedReader getReader() {
    return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(this.requestBody)));
  }

  public String read() throws IOException {
    BufferedReader reader = this.getReader();
    String line;
    StringBuilder payloadBuilder = new StringBuilder();
    while ((line = reader.readLine()) != null) {
      payloadBuilder.append(line).append(System.lineSeparator());
    }
    return payloadBuilder.toString();
  }

}

@Override
  public ServletInputStream getInputStream() {
    return new ServletInputStreamWrapper(this.requestBody);
  }

因为这里需要返回 ServletInputStream所以还需要包装一个类,因此有了ServletInputStreamWrapper。

 ServletInputStreamWrapper 包装了inputStream类,以便CachedBodyHttpServletRequest进行读取。

package com.veoride.mech.api.component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import lombok.extern.slf4j.Slf4j;

/**
 * @author darmi
 */
@Slf4j
public class ServletInputStreamWrapper extends ServletInputStream {

  public static final int REACH_END = 0;

  private final InputStream inputStream;

  public ServletInputStreamWrapper(byte[] cachedBody) {
    this.inputStream = new ByteArrayInputStream(cachedBody);
  }

  @Override
  public boolean isFinished() {
    try {
      return inputStream.available() == REACH_END;
    } catch (IOException e) {
      log.error("isFinished fail", e);
    }
    return false;
  }

  @Override
  public boolean isReady() {
    return true;
  }

  @Override
  public void setReadListener(ReadListener readListener) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int read() throws IOException {
    return inputStream.read();
  }
}

3.回调业务编写。

根据自己需要完成业务代码编写。

@RestController
@RequestMapping
@Slf4j
public class Controller {

  @PostMapping("/satisfaction/callback")
  public void satisfactionCallback(@RequestBody @Valid WebhookDTO webhookDTO) {
    ...
  }

}
package com.veoride.mech.api.dto.typeform;

import com.fasterxml.jackson.annotation.JsonAlias;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * @author darmi
 */
@Getter
@Setter
@ToString
public class WebhookDTO {

  @JsonAlias("form_response")
  @NotNull
  private FormResponse formResponse;

  public Long getPhone() {
    return this.formResponse.getHidden().getPhone();
  }

  @Getter
  @Setter
  @ToString
  public static class FormResponse {

    @NotNull
    private Hidden hidden;
  }

  @Getter
  @Setter
  @ToString
  public static class Hidden {

    @NotNull
    private Long phone;
  }


}

总结

  1. 配置Type From的调查问卷信息。
  2. 做好Type From的回调配置,隐藏参数配置以及密钥的配置。
  3. 后端服务完成Type From的请求拦截,以及验证签名和业务处理。注意请求读取后的缓存,以便传递到后续过滤器中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知始行末

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值