1. 环境

基于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>

2. 加密算法的选择

使用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;
        }
    }
  1. 编写一个接口

这里简单编写一个音频转码的接口,来模拟我们开发接口的整个过程。

    @RestController
    public class TransferController {
    
        @RequestMapping("/transfer")
        public BaseResponse transfer() {
            return BaseResponse.success("转码成功");
        }
    
        @RequestMapping("/ban")
        public BaseResponse ban() {
            return BaseResponse.error(ErrorCode.VERIFY_NO_PASS);
        }
    }

4. 自定义一个拦截器

自定义拦截器,对全面请求进行拦截判断是否传递了签名和时间戳,并且判断传递过来的签名和后端计算出来的签名一不一致,还需判断传递到后端时这个签名有没有过期,如果上面这些条件有一个不成立就进行拦截,否则放行。

    @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;
            }
        }
    }

注意点:

  1. 必须要把该类交给Spring IOC容器进行管理,也就是需要在类上面添加一个注解@Component
  2. 拦截后需要给调用方一个提示,否则调用方不知道是否被拦截,所以这里需要使用request.getRequestDispatcher(“/ban”).forward(request, response);
  3. 必须要放行拦截的URL,如果不放行,会产生死循环,在这里也就是需要放行/ban接口

5. 编写一个拦截器的配置类

编写拦截器配置类,把自定义的拦截器添加到配置类中。

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
        @Resource
        private SignatureInterceptor signatureInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(signatureInterceptor).addPathPatterns("/**").excludePathPatterns("/ban");
        }
    }

注意点:

  1. 必须要有@Configuration注解
  2. 不能使用new 自定义拦截器类的方式添加进来,必须要使用注入的方式注入进来。也就是不能写成这样registry.addInterceptor(new SignatureInterceptor()).addPathPatterns(“/**”)

6. 统一接口的响应格式

创建两个工具类,一个是响应基类,一个是错误类。

响应基类: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;
        }
    }

7. 配置文件

在自定义拦截器中,我们通过@Value注解从项目的配置文件application.yml中获取apiKeyapiSecret

application.yml文件如下:

    apiKey: dhkadj123fda
    apiSecret: hgjdakf12314sdf

对应的视频教程已经上传到B站中,如果不喜欢看文字内容,也可以看视频

Q.E.D.


热爱生活,热爱程序