❃博主首页 : 「码到三十五」 ,同名公众号 :「码到三十五」,wx号 : 「liwu0213」
☠博主专栏 : <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关>
♝博主的话 : 搬的每块砖,皆为峰峦之基;公众号搜索「码到三十五」关注这个爱发技术干货的coder,一起筑基

三方接口设计是实现系统功能的关键环节。设计一个安全、高效且易于维护的接口调用方案,对于保障系统稳定性、数据安全性和用户体验至关重要。

一、接口设计的基本原则

  1. 安全性:确保接口数据的安全性,防止数据篡改、过期、重复提交等问题。
  2. 可靠性:接口应具备高可用性,能够在高并发、高负载环境下稳定运行。
  3. 易用性:接口设计应简洁明了,易于第三方系统调用和维护。
  4. 可扩展性:接口设计应考虑到未来的扩展需求,便于增加新功能或调整现有功能。

二、设计方案

1. API密钥生成与管理
  • AK(Access Key):用于标识应用,是公开的信息,主要用于标示用户或应用身份。
  • SK(Secret Key):加密认证密钥,必须严格保密。通过AK和SK的组合使用,进行请求的加密验证,确保请求发送者的身份合法性。
2. 接口鉴权机制
  • 客户端签名:客户端使用AK和请求参数(包括但不限于路径、方法、参数、时间戳等)生成签名,并将该签名放入请求头中,以进行身份验证。
  • 时间戳:每个请求都需附带当前时间的时间戳,用于校验请求的时效性,通常有效期设置为5分钟以内。
  • 流水号(Nonce):每个请求生成一个唯一的临时随机数,确保在有效期内不允许重复提交,防止重复攻击。
3. 回调地址设置
  • 第三方应用回调地址:要求第三方应用提供回调地址,用于接收异步通知和回调结果,确保数据交互的可靠性和及时性。
4. 接口API设计
  • URL设计:明确接口的地址结构,便于第三方系统调用。
  • HTTP方法:规定使用GET、POST、PUT、DELETE等HTTP方法,明确各方法的用途和适用场景。
  • 请求参数:详细列出每个接口所需的请求参数,包括必填项和可选项,以及参数的类型和格式。
  • 响应格式:统一接口响应的数据格式,如JSON,并明确响应中的字段含义和数据类型。
5. 权限划分与认证
  • appId:应用的唯一标识,用于标识开发者账号或应用实例。
  • appKey:公开的密钥,相当于账号,用于在请求中标识应用身份。
  • appSecret:私密密钥,相当于密码,用于加密和权限控制。
  • token:临时令牌,具有时效性,用于验证用户或应用的身份。在首次验证(如登录场景)时,使用appKey和appSecret申请accessToken,并设置失效时间。后续每次请求都需带上该token以表明权限通过。
6. appKey + appSecret机制
  • 首次验证:在首次验证时,使用appKey和appSecret来申请accessToken,该token带有失效时间,用于后续的请求认证。
  • 权限控制:针对同一业务的不同权限需求(如只读权限和读写权限),可通过分配不同的appKey和appSecret来实现权限划分。每个appKey和appSecret对应不同的权限级别。
7. 简化场景
  • 开放性接口:如百度、谷歌地图API等,可省去appId和appKey,将三者合一(appId = appKey = appSecret),此时appId主要用于统计用户调用接口的次数。
  • 单一权限配置:当每个用户有且仅有一套权限配置时,可去掉appKey,直接使用appId和appSecret进行身份认证和权限控制。
  • 签名验证:在调用方向服务提供方发起请求时,带上(appKey、时间戳timeStamp、随机数nonce、签名sign)。签名sign可使用(AppSecret + 时间戳 + 随机数)通过sha1、md5等算法生成。服务提供方收到请求后,生成本地签名并与收到的签名进行比对,一致则校验成功。
8. 签名字段说明
  • appId和appSecret:唯一标识和密钥,为不同的调用方分配不同的appId和appSecret。
  • 时间戳(timeStamp):以服务端当前时间为准,设置有效期(如5分钟以内),用于校验请求的时效性。
  • 流水号(nonce):临时随机数,确保在有效期内不允许重复提交,防止重复攻击。
  • 签名字段(sign):客户端传递的签名信息,包含appId和sign字段,用于验证身份和防止参数篡改。服务提供方通过验证签名来确保请求的安全性和合法性。

三、接口设计的具体步骤

1. API密钥生成与管理

  • 生成密钥:使用随机字符串或UUID生成AK和SK,确保密钥的唯一性和安全性。
  • 存储密钥:将生成的AK和SK存储在数据库或其他持久化存储中,便于管理和查询。
  • 分发密钥:通过界面、API或自助注册流程向第三方系统提供AK和SK。
  • 定期轮换密钥:定期更换SK,以减少密钥泄露的风险。
// 生成API密钥
public class ApiKeyGenerator {
    public static String generateAccessKey() {
        return UUID.randomUUID().toString();
    }

    public static String generateSecretKey() {
        return Base64.getEncoder().encodeToString(new byte[16]);
    }
}

2. 接口鉴权

接口鉴权是验证请求合法性的重要环节。可通过以下方式进行鉴权:

  • 签名验证:客户端使用AK和请求参数生成签名,并在请求头中携带签名信息。服务端接收请求后,验证签名的合法性。
  • Token验证:客户端首次验证时,使用AK和SK申请Token。后续请求中携带Token,服务端验证Token的有效性。
// 签名验证
public class SignAuthInterceptor implements HandlerInterceptor {
    private String secretKey;

    public SignAuthInterceptor(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String timestamp = request.getHeader("timestamp");
        String nonceStr = request.getHeader("nonceStr");
        String signature = request.getHeader("signature");

        if (isExpired(timestamp) || isNonceUsed(nonceStr)) {
            throw new BusinessException("Invalid request");
        }

        String calculatedSignature = calculateSignature(request, secretKey);
        if (!signature.equals(calculatedSignature)) {
            throw new BusinessException("Invalid signature");
        }

        return true;
    }

    private boolean isExpired(String timestamp) {
        long currentTime = System.currentTimeMillis() / 1000;
        long requestTime = Long.parseLong(timestamp);
        return currentTime - requestTime > 60; // 有效期为60秒
    }

    private boolean isNonceUsed(String nonceStr) {
        // 检查nonceStr是否已使用,可使用Redis等存储方式
        return false;
    }

    private String calculateSignature(HttpServletRequest request, String secretKey) throws UnsupportedEncodingException {
        Map<String, Object> params = new HashMap<>();
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String name = parameterNames.nextElement();
            String value = request.getParameter(name);
            if (!"signature".equals(name)) {
                params.put(name, URLEncoder.encode(value, "UTF-8"));
            }
        }

        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);

        StringBuilder sb = new StringBuilder();
        for (String key : keys) {
            sb.append(key).append("=").append(params.get(key)).append("&");
        }

        sb.append(secretKey);
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes("UTF-8"));
    }
}

3. 接口API设计

接口API设计包括URL、HTTP方法、请求参数和响应格式等细节。设计时应遵循RESTful原则,使接口更加简洁、易于理解。

  • URL设计:使用简洁、描述性的URL路径,如/api/resources表示获取资源列表。
  • HTTP方法:使用GET、POST、PUT、DELETE等HTTP方法分别表示不同的操作。
  • 请求参数:明确请求参数的类型、必填项和默认值,便于调用者理解和使用。
  • 响应格式:采用统一的响应格式,如JSON,包含状态码、消息和数据等字段。
// API接口设计
@RestController
@RequestMapping("/api/resources")
public class ResourceController {

    @GetMapping
    public Result<List<Resource>> getResources(@RequestParam(defaultValue = "1") int page,
                                               @RequestParam(defaultValue = "10") int limit) {
        // 实现获取资源列表的逻辑
        List<Resource> resources = new ArrayList<>();
        return Result.success(resources);
    }

    @PostMapping
    public Result<Resource> createResource(@RequestBody Resource resource) {
        // 实现创建资源的逻辑
        return Result.success(resource);
    }

    @PutMapping("/{resourceId}")
    public Result<Void> updateResource(@PathVariable String resourceId, @RequestBody Resource resource) {
        // 实现更新资源的逻辑
        return Result.success();
    }

    @DeleteMapping("/{resourceId}")
    public Result<Void> deleteResource(@PathVariable String resourceId) {
        // 实现删除资源的逻辑
        return Result.success();
    }
}

public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("Success");
        result.setData(data);
        return result;
    }

    // Getter and Setter methods
}

4. 安全性考虑

  • 使用HTTPS:保护数据传输安全,防止中间人攻击。
  • 请求验签:服务端进行校验和鉴权,确保请求的合法性。
  • 敏感数据加密传输:使用TLS加密敏感数据,防止数据泄露。
  • 防止重放攻击:使用Nonce和Timestamp,确保请求的唯一性和时效性。
// 防止重放攻击
public class AntiReplayInterceptor implements HandlerInterceptor {
    private RedisTemplate<String, String> redisTemplate;

    public AntiReplayInterceptor(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String nonceStr = request.getHeader("nonceStr");
        String timestamp = request.getHeader("timestamp");

        if (isNonceUsed(nonceStr) || isExpired(timestamp)) {
            throw new BusinessException("Invalid request");
        }

        return true;
    }

    private boolean isNonceUsed(String nonceStr) {
        return redisTemplate.hasKey(nonceStr);
    }

    private boolean isExpired(String timestamp) {
        long currentTime = System.currentTimeMillis() / 1000;
        long requestTime = Long.parseLong(timestamp);
        return currentTime - requestTime > 60;
        // 有效期为60秒
    }
}

三、接口调用的最佳实践

  1. 合理使用Token:使用Token减少用户名和密码的传输次数,提高接口调用的安全性。
  2. 错误处理:在调用接口时,应妥善处理可能出现的错误,如网络异常、参数错误等。
  3. 日志记录:记录接口调用的日志,便于问题排查和性能分析。
  4. 接口限流:对接口进行限流,防止恶意攻击或滥用资源。

关注公众号[码到三十五]获取更多技术干货 !

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐