调查问卷Type Form的集成

简介

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

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

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

Type Form 配置

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

调查问卷Type Form的集成_第1张图片

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

调查问卷Type Form的集成_第2张图片

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

调查问卷Type Form的集成_第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的请求拦截,以及验证签名和业务处理。注意请求读取后的缓存,以便传递到后续过滤器中。

你可能感兴趣的:(java,开发语言)