基于SpringBoot编写一个接口,提供给第三方调用。类似于我们使用阿里的语音识别功能,我们可以调用阿里封装好的api,也就是通过发送HTTP请求的方式来做语音识别。本篇文章主要记录在SpringBoot中我们是如何开发接口并让别人可以安全调用的。
使用到的依赖:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.lukeewin</groupId>
<artifactId>Signature</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Signature</name>
<description>Signature</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
使用MD5这种算法加密是不太安全的,所以这里我们使用hash算法中的HmacSHA256
加密算法来生成签名,当我们请求接口时,我们使用把签名和时间戳带上,为啥还要带上时间戳呢,是因为我们之后要控制签名的过期时间需要根据这个前端传递过来的时间戳来计算过期时间。
下面是加密工具类:
public class SignatureUtil {
public static String getSignature(String timestamp, String apiKey, String apiSecret) {
// 构建签名字符串
String signatureString = apiKey + timestamp;
String signature = null;
// 计算签名
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] signatureBytes = sha256Hmac.doFinal(signatureString.getBytes(StandardCharsets.UTF_8));
signature = Base64.getEncoder().encodeToString(signatureBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
return signature;
}
}
这里简单编写一个音频转码的接口,来模拟我们开发接口的整个过程。
@RestController
public class TransferController {
@RequestMapping("/transfer")
public BaseResponse transfer() {
return BaseResponse.success("转码成功");
}
@RequestMapping("/ban")
public BaseResponse ban() {
return BaseResponse.error(ErrorCode.VERIFY_NO_PASS);
}
}
自定义拦截器,对全面请求进行拦截判断是否传递了签名和时间戳,并且判断传递过来的签名和后端计算出来的签名一不一致,还需判断传递到后端时这个签名有没有过期,如果上面这些条件有一个不成立就进行拦截,否则放行。
@Component
public class SignatureInterceptor implements HandlerInterceptor {
@Value("${apiKey}")
private String apiKey;
@Value("${apiSecret}")
private String apiSecret;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String sign = request.getParameter("sign");
String timestamp = request.getParameter("timestamp");
if (StringUtils.isNotBlank(sign) && StringUtils.isNotBlank(timestamp)) {
String signature = SignatureUtil.getSignature(timestamp, apiKey, apiSecret);
if (StringUtils.isNotBlank(signature) && signature.equals(sign) && System.currentTimeMillis() - Long.parseLong(timestamp) < 50 * 1000) {
return true;
} else {
request.getRequestDispatcher("/ban").forward(request, response);
return false;
}
} else {
request.getRequestDispatcher("/ban").forward(request, response);
return false;
}
}
}
注意点:
@Component
request.getRequestDispatcher("/ban").forward(request, response);
URL
,如果不放行,会产生死循环,在这里也就是需要放行/ban
接口编写拦截器配置类,把自定义的拦截器添加到配置类中。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private SignatureInterceptor signatureInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signatureInterceptor).addPathPatterns("/**").excludePathPatterns("/ban");
}
}
注意点:
@Configuration
注解registry.addInterceptor(new SignatureInterceptor()).addPathPatterns("/**")
创建两个工具类,一个是响应基类,一个是错误类。
响应基类:BaseResponse
@Data
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = 4L;
private Integer code;
private String message;
private Long timestamp = System.currentTimeMillis();
private T data;
public static <T> BaseResponse<T> success(T data) {
BaseResponse<T> resultData = new BaseResponse<>();
resultData.setCode(200);
resultData.setMessage("OK");
resultData.setData(data);
return resultData;
}
public static BaseResponse error(ErrorCode errorCode) {
BaseResponse resultData = new BaseResponse();
resultData.setCode(errorCode.getCode());
resultData.setMessage(errorCode.getMessage());
return resultData;
}
}
这里用到了@Data
注解,是lombok
提供的一个注解,所以你需要在pom.xml
中引入lombok
依赖。
编写错误码类:ErrorCode
public enum ErrorCode {
VERIFY_NO_PASS(300, "签名验证未通过");
private final Integer code;
private final String message;
ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
在自定义拦截器中,我们通过@Value
注解从项目的配置文件application.yml
中获取apiKey
和apiSecret
。
application.yml
文件如下:
apiKey: dhkadj123fda
apiSecret: hgjdakf12314sdf