接口书写规范和示例

API接口设计规范

参考下图:


主要分为三个方面:安全性,幂等性,数据规范

一、安全性

1.调用接口的先决条件——token

生成token的方式有多种,这里介绍使用JWT(JSON Web Token)生成token。

JWT生成token

组成

由三部分组成: 头部 + 载荷 + 签名
1.头部
用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。例如:

{
   "typ": "JWT",
  "alg": "HS256"
}

2.载荷
其实就是自定义的数据,一般存储用户Id,过期时间等信息。也就是JWT的核心所在,因为这些数据就是使后端知道此token是哪个用户已经登录的凭证。而且这些数据是存在token里面的,由前端携带,所以后端几乎不需要保存任何数据。例如:

{
  "uid": "xxxxidid",  //用户id
  "exp": "12121212"  //过期时间
}

3.签名
签名其实就是:
①头部和载荷各自base64加密后用.连接起来,然后就形成了xxx.xx的前两段token。
②最后一段token的形成是,前两段加入一个密匙用HS256算法或者其他算法加密形成。
③所以token3段的形成就是在签名处形成的。

2.使用POST作为接口

一般调用接口最常用的两种方式就是GET和POST。两者的区别也很明显,GET请求会将参数暴露在浏览器URL中(默认),而且对长度也有限制。为了更高的安全性,所有接口都采用POST方式请求(相对安全)。

3.客户ip白名单

ip白名单是指将接口的访问权限对部分ip进行开放。这样就能避免其他ip进行访问攻击,设置ip白名单比较麻烦的一点就是当你的客户端进行迁移后,就需要重新联系服务提供者添加新的ip白名单。设置ip白名单的方式很多,除了传统的防火墙之外,spring cloud alibaba提供的组件sentinel也支持白名单设置。为了降低api的复杂度,推荐使用防火墙规则进行白名单设置

4.单个接口针对ip限流

限流是为了更好的维护系统稳定性。使用redis进行接口调用次数统计,ip+接口地址作为key,访问次数作为value,每次请求value+1,设置过期时长来限制接口的调用频率。

5.记录接口请求日志

使用aop全局记录请求日志,快速定位异常请求位置,排查问题原因。

6.敏感数据脱敏

在接口调用过程中,可能会涉及到订单号等敏感数据,这类数据通常需要脱敏处理,最常用的方式就是加密。加密方式使用安全性比较高的RSA非对称加密。非对称加密算法有两个密钥,这两个密钥完全不同但又完全匹配。只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程

二、幂等性

幂等性是指任意多次请求的执行结果和一次请求的执行结果所产生的影响相同。说的直白一点就是查询操作无论查询多少次都不会影响数据本身,因此查询操作本身就是幂等的。但是新增操作,每执行一次数据库就会发生变化,所以它是非幂等的。
幂等问题的解决有很多思路,这里讲一种比较严谨的。提供一个生成随机数的接口,随机数全局唯一。调用接口的时候带入随机数。第一次调用,业务处理成功后,将随机数作为key,操作结果作为value,存入redis,同时设置过期时长。第二次调用,查询redis,如果key存在,则证明是重复提交,直接返回错误。

三、数据规范问题

1.接口版本控制

一套成熟的API文档,一旦发布是不允许随意修改接口的。这时候如果想新增或者修改接口,就需要加入版本控制,版本号可以是整数类型,也可以是浮点数类型。一般接口地址都会带上版本号,http://ip:port//v1/list。

2.相应状态码规范

一个牛逼的API,还需要提供简单明了的响应值,根据状态码就可以大概知道问题所在。我们采用http的状态码进行数据封装,例如200表示请求成功,4xx表示客户端错误,5xx表示服务器内部发生错误。

3.统一响应数据格式

为了方便给客户端响应,响应数据会包含三个属性,状态码(code),信息描述(message),响应数据(data)。客户端根据状态码及信息描述可快速知道接口,如果状态码返回成功,再开始处理数据。

code:200,
message:'success'
data {
}

接口示例

视频应用开发平台中,接口的示例

一、状态码显示

1.通过枚举类进行状态码例举

public enum CodeEnum {
    // 根据业务需求进行添加
    SUCCESS(200,"处理成功"),
    ERROR_PATH(404,"请求地址错误"),
    ERROR_SERVER(505,"服务器内部发生错误");

    private int code;
    private String message;

    CodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() { return code;}
    public void setCode(int code) {this.code = code;}

    public String getMessage() {return message;}
    public void setMessage(String message) {this.message = message;}

}

2.不使用枚举,封装时填入

在数据封装时,即(code+message+data打包时),直接将code填入,定义final的实体。

    public static final ObjectResult SUCCESS = new ObjectResult("200", "操作成功");
    public static final ObjectResult ERROR = new ObjectResult("400", "操作失败");
    public static final ObjectResult EXCEPTION = new ObjectResult("500", "服务异常");

二、统一返回数据格式

使用Object统一打包的形式,让所有Controller类的返回类型为这个统一的数据类型。数据填入data中,并带上返回码code和返回信息message。

public class ObjectResult<T> implements Serializable {
    public static final ObjectResult SUCCESS = new ObjectResult("200", "操作成功");
    public static final ObjectResult ERROR = new ObjectResult("400", "操作失败");
    public static final ObjectResult EXCEPTION = new ObjectResult("500", "服务异常");
    @ApiModelProperty(
        value = "状态码",
        required = false,
        example = "200"
    )
    private String code;
    @ApiModelProperty(
        value = "错误信息",
        required = false,
        example = "操作成功"
    )
    private String msg;
    @ApiModelProperty(
        value = "返回数据",
        required = false
    )
    private T data;
        public ObjectResult(String code, String msg) {
        this(code, msg, (Object)null);
    }

    public ObjectResult(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public String getCode() {
        return this.code;
    }

    public String getMsg() {
        return this.msg;
    }

    public T getData() {
        return this.data;
    }

    public void setCode(final String code) {
        this.code = code;
    }

    public void setMsg(final String msg) {
        this.msg = msg;
    }

    public void setData(final T data) {
        this.data = data;
    }
}

三、接口逻辑

接口以MVC三层模型来写,从上至下,从表面至底层:

①表层:Controller类,包括简洁的业务逻辑。使用url方式调用。
②业务层:Service,定义了给Controller实现业务逻辑的接口。
③业务实现层:ServiceImpl,service接口的代码实现。
④数据库配置层:Mapper接口,ServiceImpl内访问DAO层的接口定义
⑤持久层SQL:MapperXML,对mapper接口的配置,实现访问数据库的SQL

1.表层:Controller

使用URL做标注,提供给外部访问。
在本项目中,所有的Controller都继承了BaseController,而BaseController又继承了Base类。
在Base中,定义了获取用户id等方法,调用了获取token方法。为所有的控制器类Controller添加了鉴权功能。
本项目中,使用的时JWT的token生成。

前后端分离
方法的类型标记为ObjectResult,统一接口返回的数据类型
注入私有的Service服务
内部简洁:只包括Service服务的调用,不包含任何代码逻辑

示例:

@RestController
@RequestMapping("web/product")
@Api(value = "产品管理", tags = "产品管理 web端接口")
@Slf4j
@Scope("prototype")
public class ProductController extends BaseController<Product> {

    @Resource
    private ProductService service;

    @ApiOperation(value = "添加或修改product", notes = "添加product")
    @PostMapping("/save")
    @UseYunYaoToken
    @NeedOperateRecord(devLogModuleType = DevLogModuleType.PRODUCTMGR,devLogActionType = DevLogActionType.PRODUCTMGR_SAVE)
    public ObjectResult productSave(@RequestBody @Valid SaveProductDto saveProductDto) {
        Long productId = service.saveOrUpdate(saveProductDto);
        return ResultUtil.success(productId);
    }

    @ApiOperation(value = "根据id删除product", notes = "根据id删除product")
    @PostMapping("/deleteByIds")
    @UseYunYaoToken
    @NeedOperateRecord(devLogModuleType = DevLogModuleType.PRODUCTMGR,devLogActionType = DevLogActionType.PRODUCTMGR_DELETE)
    public ObjectResult productDelete(@RequestBody @Valid List<String> dtos){
        List<Long> productIds = service.deleteByIds(dtos);
        return ResultUtil.success(productIds);
    }
}

2.业务层:Service

Service都是接口interface,定义了服务的功能。
示例:

public interface ProductService extends IService<Product> {
    /**
     * 保存或更新产品
     **/
    Long saveOrUpdate(SaveProductDto saveProductDto);

    /**
     * 按id删除产品
     **/
    List<Long> deleteByIds(List<String> dtos);
}

3.业务实现层:ServiceImpl

ServiceImpl实现Service接口的功能,内部一般会注入数据库mapper,或者其他模块的接口服务类

@Slf4j
@Service
public class ProjectServiceImpl extends ServiceImpl<ProjectMapper,Project> implements ProjectService {

    //注入了ybase服务的接口服务类
    @Autowired
    YbaseServiceManager ybaseServiceManager;

    /***
     * 删除库中project
     * @param products
     */
    @Override
    public List<Project> deleteProjects(List<Product> products) {

        List<String> productCodes = products.stream()
                .map(Product::getProductCode)
                .collect(Collectors.toList());

        //查询所有project,对比project关联的productId,过滤出要删除的project对象
        List<Project> projects = list().stream()
                .filter(x -> productCodes.contains(x.getProductCode()))
                .collect(Collectors.toList());

        //删除库中project
        removeByIds(projects.stream()
                .map(Project::getId)
                .collect(Collectors.toList()));

        return projects;
    }
}

4.数据库配置层:mapper

Mapper中定义了访问数据库的接口功能。
示例:

@Mapper
public interface ProductMapper extends BaseMapper<Product> {

    List<Product> selectByConditions(Map<String,Object> params);

    Product selectByCode(String productCode);

}

5.持久层SQL

这里通过相应的持久层框架,访问数据库,获取数据库结果和操作数据库。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hikvision.hikkan.vsads.console.mapper.ProductMapper">
    <select id="selectByCode" parameterType="String" resultType="com.hikvision.hikkan.vsads.console.entity.Product">
        select id, user_id, dev_from, product_name, product_code, product_logo, resolution_id, create_time, update_time, isdeleted
        from tb_product
        <where>
            <if test="productCode != null">
                product_code=#{productCode}
            </if>
            and isdeleted = false
        </where>
    </select>
</mapper>

四、数据传输定义

视频应用开发平台中,主要使用了三种实体。

①POJO(Plain Ordinary Java Object):即简单普通java对象。
②DTO(Data Transfer Object):即数据传输对象。
③VO(Value Object/View Object):即值对象或页面对象。

1.POJO,简单普通java对象

在本项目中,POJO别名entity,放入了entity包中。
POJO的作用:一般用在数据层映射到数据库表的类,类的属性与表字段一一对应。
示例:

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_product")
@ApiModel(value="Product对象", description="")
public class Product extends Model<Product> {
    //版本信息
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "编号")
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "产品名称")
    @TableField("product_name")
    private String productName;
}

2.DTO,数据传输对象

全称为:Data Transfer Object,即数据传输对象。一般用于向数据层外围提供仅需的数据,如查询一个表有50个字段,界面或服务只需要用到其中的某些字段,DTO就包装出去的对象。可用于隐藏数据层字段定义,也可以提高系统性能,减少不必要字段的传输损耗。
这里可以对数据进行一定的限制,比如saveProductDTO,密码要在9-16位之间,用户名不能为空;产品名不能超过16位等。
使用注解来进行限制:@Length,@NotNull,@Pattern
示例:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SaveProductDto {

    @ApiModelProperty(value = "产品名称")
    @Length(max = 16, message = "产品名称:不能超过16个字符")
    @NotNull
    private String name;

    @ApiModelProperty(value = "产品标识")
    @Pattern(regexp = "^$|^[a-z0-9_]+$", message = "应用标识:必须是数字、小写字母、下划线组成")
    @NotNull
    private String code;

    @ApiModelProperty(value = "负责人用户id")
    @NotEmpty(message = "负责人不能为空")
    private String principle;
}

3.VO,页面对象

全称为:Value Object,有的也称为View Object,即值对象或页面对象。一般用于web层向view层封装并提供需要展现的数据。
示例:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductItemVo {
    /***
     * 应用id
     */
    private String id;

    /***
     * 应用名称
     */
    private String name;

    public static ProductItemVo createProductItemVo(String id, String productName, String productCode) {
        return new ProductItemVo(id, productName, null, productCode, null);
    }
}