如何设计一个安全的对外接口?

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 对外接口安全措施的作用主要体现在两个方面,一方面是如何保证数据在传输过程中的安全性,另一方面是数据已经到达服务器端,服务器端如何识别数据。

对外接口安全措施的作用主要体现在两个方面,一方面是如何保证数据在传输过程中的安全性,另一方面是数据已经到达服务器端,服务器端如何识别数据。

  1. 数据加密

数据在传输过程中是很容易被抓包的,如果直接传输,数据可以被任何人获取,所以必须对数据加密。加密方式有对称加密和非对称加密:

对称加密:对称密钥在加密和解密的过程中使用的密钥是相同的,常见的对称加密算法有 DES、AES。优点是计算速度快,缺点是在数据传送前,发送方和接收方必须商定好密钥,并完好保存。如果一方的密钥被泄露,那么加密信息也就不安全了;
非对称加密:服务端会生成一对密钥,私钥存放在服务器端,公钥可以发布给任何人使用。与对称加密相比,这种方式更安全,但速度慢太多了。

现在主流的做法是使用 HTTPS 协议,在 HTTP 和 TCP 之间添加一层加密层 (SSL 层),这一层负责数据的加密和解密。HTTPS 的实现方式结合了对称加密与非对称加密的优点,在安全和性能方面都比较突出。
示例
javascript
const CryptoJS = require("crypto-js");
// 密钥,8 字节
const key = CryptoJS.enc.Utf8.parse('12345678');
// 偏移量,8 字节
const iv = CryptoJS.enc.Utf8.parse('12345678');

// DES 加密
function encryptDES(data) {
const encrypted = CryptoJS.DES.encrypt(data, key, {

iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7

});
return encrypted.toString();
}

// DES 解密
function decryptDES(encryptedData) {
const decrypted = CryptoJS.DES.decrypt(encryptedData, key, {

iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7

});
return decrypted.toString(CryptoJS.enc.Utf8);
}

// 测试
const originalData = 'Hello, world!';
const encryptedData = encryptDES(originalData); //YcunRrQmVq9nAmF4fyALkw==
console.log('加密后的数据:', encryptedData);
const decryptedData = decryptDES(encryptedData);
console.log('解密后的数据:', decryptedData);
复制代码
java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Component;

public class DesUtil {


// 密钥,长度为8个字符
private static final String KEY = "12345678";

// 偏移量,长度为8个字符
private static final String IV = "12345678";

// 加密算法
private static final String ALGORITHM = "DES";

// 加密模式
private static final String TRANSFORMATION = "DES/CBC/PKCS5Padding";

// 加密
public static String encrypt(String input) throws Exception {
    // 创建密钥对象
    SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
    // 创建偏移量对象
    IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
    // 创建加密器对象
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    // 初始化加密器
    cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
    // 加密
    byte[] encrypted = cipher.doFinal(input.getBytes());
    // 将加密后的字节数组转换为Base64字符串
    return Base64.encodeBase64String(encrypted);
}

// 解密
public static String decrypt(String input) throws Exception {
    // 创建密钥对象
    SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
    // 创建偏移量对象
    IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
    // 创建解密器对象
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    // 初始化解密器
    cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
    // 将Base64字符串解码为字节数组
    byte[] decrypted = cipher.doFinal(Base64.decodeBase64(input));
    // 将解密后的字节数组转换为字符串
    return new String(decrypted);
}

// 测试
public static void main(String[] args) throws Exception {
    String originalData = "Hello, world!";
    String encryptedData = encrypt(originalData);
    System.out.println(encryptedData); // YcunRrQmVq9nAmF4fyALkw==
}  

}
复制代码

  1. 数据加签

数据加签就是由发送者产生一段无法伪造的数字串,来保证数据在传输过程中不被篡改。数据如果已经通过 HTTPS 加密了,其加密部分只是在外网,而加签可以防止内网中数据被篡改。
数据签名使用较多的是 MD5 算法,将需要提交的数据通过某种方式组合,然后通过 MD5 生成一段加密字符串,这段加密字符串就是数据包的签名。而其中的用户密钥,客户端和服务端都保留一份,会更加安全。
示例
java
定义一个工具类来实现带密钥的MD5加签和验签:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

public static String md5(String data, String key) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update((data + key).getBytes());
        byte[] digest = md.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("MD5加签失败", e);
    }
}

public static boolean verify(String data, String sign, String key) {
    return md5(data, key).equals(sign);
}

}

复制代码
在业务代码中使用该工具类进行MD5加签和验签:
@RestController
public class DemoController {

private static final String KEY = "123456";

@GetMapping("/demo")
public String demo(String data, String sign) {
    // 对数据进行MD5加签
    String signData = MD5Util.md5(data, KEY);
    if (signData.equals(sign)) {
        // 验签通过
        return "验签通过";
    } else {
        // 验签不通过
        return "验签不通过";
    }
}

}
复制代码
javascript
定义一个工具类来实现带密钥的 MD5 加签和验签:
const CryptoJS = require("crypto-js");

const MD5Util = {
md5(data, key) {

// 使用 crypto-js 库计算 MD5 值
const hash = CryptoJS.MD5(data + key); 
return hash.toString();

},

verify(data, sign, key) {

// 计算数据的签名
const dataSign = this.md5(data, key); 
// 返回验签结果
return dataSign === sign; 

}
};
复制代码
在业务代码中使用该工具类进行 MD5 加签和验签:
const KEY = '123456';

function demo(data, sign) {
// 对数据进行 MD5 加签
const signData = MD5Util.md5(data, KEY);
if (signData === sign) {

// 验签通过
return '验签通过';

} else {

// 验签不通过
return '验签不通过';

}
}

复制代码

  1. 时间戳机制

数据经过如上的加密、加签处理后,就算被抓包也不能看到真实的数据。但是有的不法者不关心真实数据,而是直接拿到抓取的数据包进行恶意请求。这时可以使用时间戳机制,在每次请求中加入当前的时间,服务器端会拿到当前时间与消息中的时间相减,看看是否在一个固定的时间范围内,比如 5 分钟,这样恶意请求的数据包是无法更改时间的,所以 5 分钟后就视为非法请求了。
java
定义一个拦截器:
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class TimeStampInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 获取请求头中的时间戳
    String timeStampStr = request.getHeader("timeStamp"); 
    if (timeStampStr != null) {
        // 将时间戳转换为 long 类型
        long timeStamp = Long.parseLong(timeStampStr); 
        // 获取当前时间戳
        long now = System.currentTimeMillis(); 
        // 验证时间戳是否有效
        if (TimeUnit.MILLISECONDS.toMinutes(now - timeStamp) < 5) { 
            return true;
        }
    }
    response.getWriter().print("时间戳无效!");
    response.getWriter().flush();
    response.getWriter().close();
    return false;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}

}
复制代码
将拦截器注册到springboot中:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
      // 添加需要拦截的接口路径
    registry.addInterceptor(new TimeStampInterceptor())
            .addPathPatterns("/api/**"); 
}

}
复制代码

  1. AppID 机制

大部分网站都需要用户名和密码才能登录,这其实也是一种安全机制。相应的对外接口也需要这么一种机制,使用接口的用户需要在后台开通 AppID,提供给用户相关的密钥。在调用的接口中需要提供 AppID+ 密钥,服务器端会进行相关的验证。生成唯一的 AppID 即可,根据实际情况看是否需要全局唯一,同时密钥使用字母、数字等特殊字符随机生成。
java
定义一个拦截器:
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class AppIdSecretInterceptor implements HandlerInterceptor {

@Value("${app.id}")
private String appId;

@Value("${app.secret}")
private String appSecret;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      // 获取请求头中的 AppID
    String appIdParam = request.getHeader("appId"); 
      // 获取请求头中的密钥
    String secretParam = request.getHeader("secret"); 
      // 验证 AppID 和密钥是否正确
    if (appId.equals(appIdParam) && DigestUtils.md5Hex(appIdParam + appSecret).equals(secretParam)) { 
        return true;
    }
    response.getWriter().print("AppID 或密钥错误!");
    response.getWriter().flush();
    response.getWriter().close();
    return false;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}

}

复制代码
将拦截器注册到springboot中:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
      // 添加需要拦截的接口路径
    registry.addInterceptor(new AppIdSecretInterceptor())
            .addPathPatterns("/api/**"); 
}

}
复制代码

  1. 限流机制

本来就是真实的用户,并且开通了 AppID,但出现了频繁调用接口的情况,这时需要给相关 AppID 限流处理,常用的限流算法包括:令牌桶限流、漏桶限流、计数器限流。
漏桶算法的原理是按照固定常量速率流出请求,流入请求速率任意,当请求数超过桶的容量时,新的请求等待或者拒绝服务,漏桶算法可以强制限制数据的传输速度。

令牌桶算法的原理是令牌桶算法 和漏桶算法 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

计数器算法比较简单粗暴,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数。计数器限流只要一定时间内的总请求数超过设定的阀值,就会进行限流。
java
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RateLimitInterceptor implements HandlerInterceptor {

    // 创建一个令牌桶,每秒放行 10 个请求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(10); 

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 尝试获取一个令牌
    if (RATE_LIMITER.tryAcquire()) { 
        return true;
    }
    response.getWriter().print("请求过于频繁,请稍后重试!");
    response.getWriter().flush();
    response.getWriter().close();
    return false;
}

}

复制代码
以上代码中,我们定义了一个拦截器 RateLimitInterceptor,并创建了一个 RATE_LIMITER 对象作为令牌桶,每秒放行 10 个请求。在 preHandle 方法中,我们使用 RATE_LIMITER.tryAcquire() 方法尝试获取一个令牌,如果获取成功,则返回 true,否则拒绝调用接口并返回 false。
拦截器注册到 Spring Boot:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 添加需要拦截的接口路径
    registry.addInterceptor(new RateLimitInterceptor())
            .addPathPatterns("/api/**"); 
}

}
复制代码

  1. 黑名单机制

如果一个 AppID 进行过很多非法操作,或者专门有一个中黑系统,经过分析之后可以直接将此 AppID 列入黑名单,所有请求直接返回错误码。如何查看黑名单列表呢?可以给用户设置一个状态比如:初始化状态、正常状态、中黑状态、关闭状态等等。或者直接通过分布式配置中心,保存黑名单列表,每次检查用户是否在列表中即可。
java
创建一个黑名单服务,用于管理被禁用的 IP:
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class BlacklistService {

    // 存储被禁用的 IP
private Set<String> blackList = new HashSet<>(); 

public void add(String ip) {
    blackList.add(ip);
}

public boolean contains(String ip) {
    return blackList.contains(ip);
}

}
复制代码
定义一个拦截器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BlacklistInterceptor implements HandlerInterceptor {

@Autowired
private BlacklistService blacklistService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String ip = request.getRemoteAddr();
    if (blacklistService.contains(ip)) {
        response.getWriter().print("您的 IP 已被禁用!");
        response.getWriter().flush();
        response.getWriter().close();
        return false;
    }
    return true;
}

}
复制代码
拦截器注册到 Spring Boot:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 添加需要拦截的接口路径
    registry.addInterceptor(new BlacklistInterceptor())
            .addPathPatterns("/api/**"); 
}

}
复制代码

  1. 数据合法性校验

这是每个系统都会有的处理机制,只有在数据合法的情况下才会进行数据处理。每个系统都有自己的验证规则,当然也可能有一些常规性的规则,比如身份证号码长度和组成、电话号码长度和组成等等。
合法性校验包括常规性校验以及业务校验,前者包括签名校验、必填校验、长度校验、类型校验、格式校验等,后者根据实际业务而定,比如订单金额不能小于 0 等等。

相关文章
|
7月前
|
运维 Serverless API
函数计算产品使用问题之作为api网关后端服务,切到3.0有什么好处
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
8月前
|
安全 前端开发 NoSQL
如果让你设计一个接口,你会考虑哪些问题?
接口设计需关注参数校验、扩展性、幂等性、日志、线程池隔离、异常重试、异步处理、查询优化、限流、安全性、锁粒度和避免长事务。入参与返回值校验确保数据正确性;考虑接口扩展性以适应不同业务需求;幂等设计防止重复操作;关键接口打印日志辅助问题排查;核心接口使用线程池隔离确保稳定性;异常处理中可采用重试机制,注意超时控制;适合异步的场景如用户注册后的通知;并行查询提升性能;限流保护接口,防止过载;配置黑白名单保障安全;适当控制锁粒度提高并发性能;避免长事务影响系统响应。
111 2
|
8月前
|
消息中间件 设计模式 监控
如何优雅地实现接口统一调用?
【2月更文挑战第6天】
450 3
|
程序员 C++
论接口的封装能力
论接口的封装能力
54 0
|
算法 安全 网络协议
如何设计一个安全的对外接口 ?
最近有个项目需要对外提供一个接口,提供公网域名进行访问,而且接口和交易订单有关,所以安全性很重要;这里整理了一下常用的一些安全措施以及具体如何去实现。
120 0
|
存储 搜索推荐 API
如何设计 RPC 接口
如何设计 RPC 接口
262 0
|
JSON 自然语言处理 前端开发
跨端架构下客户端侧API维护方案总结
淘宝App搜索业务侧采用的是局部动态化的跨端技术架构,客户端提供丰富的基础能力与视图组件的API,前端负责业务视图搭建与业务逻辑实现。
135 0
|
消息中间件 NoSQL JavaScript
业务开发时,接口不能对外暴露怎么办?
业务开发时,接口不能对外暴露怎么办?
|
算法 安全 网络协议
如何设计一个安全的对外接口
如何设计一个安全的对外接口
329 0
|
XML SQL JSON
开源SPL,WebService/Restful广泛应用于程序间通讯,如微服务、数据交换、公共或私有的数据服务等。
开源SPL,WebService/Restful广泛应用于程序间通讯,如微服务、数据交换、公共或私有的数据服务等。
155 0
开源SPL,WebService/Restful广泛应用于程序间通讯,如微服务、数据交换、公共或私有的数据服务等。