☰
阿里一面:如何保证API接口数据安全?
2026/10/28 1:46:26 网站建设 项目流程

一、为什么API接口需要做数据安全防护?

在互联网架构中,API 接口已经成为系统与系统、客户端与服务端之间最重要的交互通道。一个订单服务可能同时被 App、小程序、Web、第三方商家和内部运营系统调用,任何一条裸露的接口都可能成为攻击者的突破口。API 数据安全的核心诉求,并不是“把门锁死”,而是在保证可用性的前提下,做到身份可信、权限可控、数据不泄露、请求不可伪造、攻击可追溯。

常见的 API 安全风险可以归纳为几类:传输过程被窃听、身份凭证被伪造、越权访问他人数据、参数注入攻击、重放攻击、接口被恶意刷量、敏感信息在前端或日志中泄露等。对于阿里这类体量的系统,接口安全往往通过网关层、服务层、数据层多层协同来保障,面试官问这个问题时,考察的是候选人是否具备体系化的安全思维,而不是只会回答“加个 Token 就行”。

从面试回答的框架来说,可以按“传输安全、身份认证、授权控制、输入校验、防篡改、防重放、幂等、限流审计”这一条链路来展开。下面沿着这条主链路,把每一层要解决的问题、常用的技术方案和关键代码示例逐一讲透。

二、传输层安全:HTTPS 与 TLS

API 安全的第一道闸门是传输层。HTTP 协议是明文传输,攻击者通过中间人劫持可以轻易拿到请求参数、Token、手机号、密码等敏感数据,甚至篡改响应内容。因此生产环境的 API 必须走 HTTPS,也就是在 TCP 与 HTTP 之间插入 TLS 层,通过证书和密钥协商建立加密通道。

理解 HTTPS 要抓住三个关键点:机密性、完整性和身份认证。机密性由对称加密保证,数据传输阶段使用 AES 等对称算法加密;对称密钥本身在握手阶段通过非对称加密(如 RSA、ECDHE)安全协商;完整性由消息认证码保证,防止数据被篡改;身份认证则依赖 CA 证书链,客户端验证服务端身份,防止访问到伪造的服务器。

在 Java 服务端,通常由 Nginx 或网关终结 TLS,后端走内网 HTTP。以下是一个 Nginx 开启 HTTPS 的典型配置:

nginx

server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/cert/server.crt; ssl_certificate_key /etc/nginx/cert/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://backend_cluster; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } }

仅开启 HTTPS 还不够,还需要注意证书的对称加密套件选择、证书有效期管理,以及避免使用已经废弃的 TLS 1.0/1.1。同时移动端也要做证书校验,不能因为“省事”忽略 HTTPS 证书合法性的验证,否则仍然存在中间人攻击的空间。

三、身份认证:确认“你是谁”

认证要解决的核心问题是:当前请求到底来自哪个用户或哪个服务。常见的认证方案包括 Session、Token、JWT、OAuth2.0,以及内部服务之间的签名认证。

3.1 Session 与 Token 的对比

传统 Web 应用常用 Session,服务端把会话状态存在 Redis 或内存中,客户端只保存一个 SessionId。优点是服务端可控,可以主动踢人下线;缺点是服务端有状态,水平扩容时需要共享会话存储,并且不适合纯 API 场景。

Token 则是一种无状态凭证。用户登录后服务端签发 Token,后续请求携带 Token,服务端通过解析和校验识别用户。访问令牌通常有过期时间,配合刷新令牌可以实现长期登录。Token 方案更适合移动端和前后端分离架构,但令牌一旦泄露,攻击者就可以冒充用户,因此需要配合短有效期、刷新机制和黑名单。

下面是一个基于 Redis 的 Token 签发与校验示例:

java

public class TokenService { private StringRedisTemplate redis; public String issueToken(String userId) { String token = UUID.randomUUID().toString().replace("-", ""); // 30 分钟过期,redis 中保存用户会话 redis.opsForValue().set("token:" + token, userId, 30, TimeUnit.MINUTES); return token; } public String getUserId(String token) { if (token == null || token.isEmpty()) { throw new BizException("缺少访问令牌"); } String userId = redis.opsForValue().get("token:" + token); if (userId == null) { throw new BizException("令牌已失效或不存在"); } return userId; } }

3.2 JWT 的结构与使用

JWT 是无状态 Token 的代表,由 Header、Payload、Signature 三部分组成,点号分隔。Header 声明算法,Payload 携带用户信息和过期时间,Signature 用密钥对前两部分签名。服务端收到 JWT 后不查存储,只校验签名和过期时间即可识别用户。

JWT 的最大优势是天然无状态、跨服务传递方便,适合微服务网关鉴权。但它也有明显短板:一旦签发,在过期前无法主动吊销;Payload 只是 Base64 编码不是加密,不能放敏感信息。

以下是一个使用 jjwt 库签发和解析 JWT 的示例:

java

import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public class JwtUtil { private static final String SECRET = "replace-with-a-long-random-secret"; public static String createToken(String userId) { return Jwts.builder() .setSubject(userId) .claim("role", "user") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000L)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

3.3 OAuth2.0 与开放平台授权

当系统需要把自己的数据开放给第三方应用,或者接入第三方登录时,OAuth2.0 是事实标准。它的核心是把“用户授权”和“访问令牌”解耦,第三方应用通过授权码、客户端凭证等方式拿到令牌,再访问资源。

OAuth2.0 有四种常见授权模式:授权码模式、简化模式、密码模式、客户端凭证模式。开放平台一般只使用授权码模式和客户端凭证模式,密码模式因为要求第三方持有用户账号密码,安全风险高,尽量弃用。

面试中如果能讲清楚“为什么授权码模式比简化模式安全”,会是加分项:授权码模式中,令牌不经过浏览器前端,前端只能拿到一次性的授权码,配合客户端密钥才能在服务端换取令牌,从而降低令牌被浏览器侧脚本或日志窃取的风险。

四、授权访问:确认“你能做什么”

认证解决“你是谁”,授权解决“你能做什么”。很多安全事件的问题不在于攻击者进不来,而在于普通用户拿到了管理员能力,或者用户 A 读到了用户 B 的数据,这就是越权。

4.1 RBAC 角色权限控制

RBAC 是目前最通用的授权模型。系统先把权限点抽象为资源加操作,再把权限点绑定到角色,最后把角色分配给用户。网关或拦截器根据用户角色判断是否放行。

下面是一个基于注解和拦截器的权限校验示例:

java

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); }

java

public class PermissionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod method = (HandlerMethod) handler; RequirePermission require = method.getMethodAnnotation(RequirePermission.class); if (require == null) { return true; } UserContext user = UserContextHolder.get(); if (user == null || !user.hasPermission(require.value())) { throw new ForbiddenException("无权限访问该接口"); } return true; } }

4.2 数据级权限与越权防护

接口级权限只是第一层,更隐蔽的是水平越权,例如用户把订单号从 1001 改成 1002,就能查看别人的订单。防护的关键是“资源归属校验”,任何基于 ID 的数据访问,都必须校验该资源是否属于当前用户。

java

@GetMapping("/orders/{orderId}") public OrderVO getOrder(@PathVariable Long orderId) { Order order = orderService.getById(orderId); if (order == null || !order.getUserId().equals(UserContextHolder.get().getUserId())) { throw new ForbiddenException("无权访问该订单"); } return toVO(order); }

更进一步,如果业务规模较大,可以在网关层做粗粒度路由级鉴权,在服务层做数据级鉴权,双保险。切勿只在前端隐藏按钮,后端不校验,这是实践中出现最多的越权漏洞根因。

五、输入校验与注入攻击防护

永远不要信任客户端传入的任何参数。输入校验是一道基础但极其重要的防线,目标是阻止 SQL 注入、XSS、命令注入、路径穿越、恶意大字段等攻击。

5.1 SQL 注入防护

SQL 注入的根源是开发者拼接 SQL 字符串,攻击者通过构造参数改变 SQL 语义,例如传入1 OR 1=1绕过条件。防护手段首选预编译语句和参数绑定,其次才是输入过滤。

使用 JDBC PreparedStatement 或 MyBatis 的参数占位符,都可以避免参数被当作 SQL 指令执行:

java

String sql = "SELECT id, user_name FROM t_user WHERE mobile = ?"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, mobile); try (ResultSet rs = ps.executeQuery()) { // 处理结果 } }

使用 MyBatis 时,要优先写#{mobile}而不是${mobile},因为后者会把参数直接拼接进 SQL。动态排序字段如果必须使用${},也应先做白名单校验,例如只允许id、create_time等固定字段。

5.2 XSS 防护

XSS 攻击指攻击者把脚本注入页面,窃取 Cookie 或 Token。对 API 来说,存储型 XSS 更常见:用户提交的内容被存储后,在管理后台或其他页面未转义地展示,导致脚本执行。防护要点是“输出转义、输入清洗、敏感字段过滤”。

在服务端输出到 HTML 场景时,需要对<、>、引号等做转义:

java

public class HtmlEscape { public static String escape(String raw) { if (raw == null) { return ""; } return raw.replace("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;") .replace("\"", "&quot;"); } }

如果内容最终以 JSON 返回给前端渲染,前端框架的默认转义也需要开启,不要在富文本场景直接使用v-html或innerHTML,除非内容经过可信清洗。

5.3 参数白名单与业务校验

除了注入攻击,还有很多业务级异常输入,比如页码过大、文件格式非法、金额为负数、手机号格式错误。推荐使用 Bean Validation 做参数校验,并在控制器统一拦截异常。

java

public class CreateOrderRequest { @NotBlank(message = "商品ID不能为空") private String skuId; @Min(value = 1, message = "数量必须大于0") private Integer count; @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") private String mobile; }

文件上传接口需要额外限制文件类型、大小,并重命名文件,禁止直接使用用户传入的文件名拼接路径,防止路径穿越。以下是文件名清洗的简单实现:

java

public String sanitizeFileName(String original) { String base = Paths.get(original).getFileName().toString(); String ext = ""; int dot = base.lastIndexOf('.'); if (dot >= 0) { ext = base.substring(dot); base = base.substring(0, dot); } return UUID.randomUUID().toString().replace("-", "") + ext; }

六、防重放攻击

重放攻击指攻击者截获合法请求后原样重复发送,达到重复扣款、重复下单等目的。仅仅加 HTTPS 并不能防重放,因为抓包者虽然看不到明文,却可以把完整密文请求再发一次。

防重放通常组合使用三个字段:时间戳、随机数 Nonce、签名。客户端请求时携带当前时间戳和一次性随机串,服务端判断时间戳是否在可接受的时间窗口内,再判断 Nonce 是否已经被用过。已用 Nonce 可以存入 Redis 并设置与时间窗口一致的过期时间。

java

public class ReplayGuard { private static final long ALLOWED_WINDOW_MILLIS = 5 * 60 * 1000L; private StringRedisTemplate redis; public boolean check(String timestamp, String nonce) { long requestTime; try { requestTime = Long.parseLong(timestamp); } catch (NumberFormatException e) { return false; } if (Math.abs(System.currentTimeMillis() - requestTime) > ALLOWED_WINDOW_MILLIS) { return false; } // 原子性写入,若已存在返回 false Boolean success = redis.opsForValue() .setIfAbsent("nonce:" + nonce, "1", ALLOWED_WINDOW_MILLIS, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(success); } }

需要补充的是,防重放与幂等不是一回事。防重放侧重同一条请求被中间人重复发送,幂等侧重客户端因网络超时重试造成的业务重复提交。两者常配合使用,但实现位置和目的不同。

七、请求防篡改与签名验签

HTTPS 能防中间人篡改,但在网关到内部服务之间、第三方回调、定时任务通知等场景中,仍然需要应用层签名来确认消息来源可信、内容未被改动。签名验签的核心思路是:双方约定密钥,把关键参数排序后拼接,用 HMAC 或非对称算法计算摘要,随请求一起发送。

以下是一个基于 HMAC-SHA256 的请求签名流程示例。签名串由 HTTP 方法、请求路径、时间戳、Nonce、排序后的业务参数组成:

java

import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TreeMap; public class SignUtil { public static String sign(Map<String, String> params, String secret) throws Exception { String canonical = buildCanonicalString(params); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] bytes = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)); return bytesToHex(bytes); } private static String buildCanonicalString(Map<String, String> params) { // 排除 sign 字段,键按字母序排序 Map<String, String> sorted = new TreeMap<>(); params.forEach((k, v) -> { if (!"sign".equals(k) && v != null) { sorted.put(k, v); } }); StringBuilder sb = new StringBuilder(); sorted.forEach((k, v) -> sb.append(k).append('=').append(v).append('&')); return sb.append("key=").append(secret).toString(); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } }

服务端收到请求后,按同样的规则重新计算签名,并与请求携带的签名比对。如果一致,说明请求内容未被篡改、时间戳在有效期内、Nonce 也未被使用过,验签通过。为了进一步降低风险,可以对密钥做版本管理,定期轮换,避免密钥泄露后长期有效。

八、幂等性设计:让重复请求不产生副作用

防重放解决的是攻击者重复发送同一请求的问题,而幂等解决的是业务层面的重复提交问题。用户网络抖动、客户端超时重试、消息中间件重投递,都可能导致同一次业务操作被执行多次。如果接口不是幂等的,就可能出现重复下单、重复扣款、重复发券等严重问题。

幂等设计的核心思路是:给每一次业务操作分配一个全局唯一标识(业务唯一键),并在服务端做去重判断。常用的实现方式包括:

  • 数据库唯一索引:以业务唯一键作为唯一索引,插入冲突时直接返回成功,不重复执行业务逻辑。

  • 状态机校验:每次操作前先检查当前状态是否允许该操作,例如订单只能从“待支付”变成“已支付”,重复调用时状态已经变更,直接返回成功。

  • Redis 去重标记:以业务唯一键作为 Redis key,SETNX 成功表示首次操作,失败表示重复操作。

  • 乐观锁版本号:更新时带上版本号,只有版本匹配才更新,避免并发重复更新。

java

public class IdempotentService { private StringRedisTemplate redis; public boolean tryLock(String bizKey, long expireSeconds) { Boolean success = redis.opsForValue() .setIfAbsent("idem:" + bizKey, "1", expireSeconds, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } public void createOrder(String requestId, OrderRequest request) { if (!tryLock(requestId, 300)) { throw new BizException("重复请求,已忽略"); } // 执行业务逻辑 orderService.create(request); } }

幂等和防重放的区别在于:防重放强调的是“同一条请求不被攻击者重复利用”,幂等强调的是“同一次业务操作不被重复执行”。两者常常一起使用,但关注点和实现位置不同。

九、限流与防刷:保护接口不被恶意打垮

即使前面所有安全措施都做到位,一个开放的 API 仍然可能被恶意刷量、被爬虫抓取、被 DDoS 攻击。限流与防刷就是为了让接口在被恶意调用时,仍然保持对正常用户的可用性。

常见的限流维度包括:

  • IP 维度:限制单个 IP 单位时间内的请求次数,防止单点刷量。

  • 用户维度:限制单个用户单位时间内的请求次数,防止账号被滥用。

  • 接口维度:对某些高消耗接口设置更严格的限流,例如发送短信、生成报表。

  • 全局维度:对整个服务设置总 QPS 上限,保护后端不被压垮。

限流算法上,常用的有固定窗口、滑动窗口、漏桶和令牌桶。实际生产中最常用的是令牌桶,因为它既支持平均速率控制,也允许一定程度的突发流量。在微服务架构中,限流通常部署在网关层(如 Sentinel、Spring Cloud Gateway)和服务层(如 Guava RateLimiter、Resilience4j RateLimiter)两个位置,形成分层防护。

十、审计日志与敏感信息防护

安全的最后一道防线是审计和追溯。任何一个敏感操作,都应该留下完整的审计日志,包括操作人、操作时间、请求参数、返回结果、客户端 IP、设备指纹等。审计日志有几个关键要求:

  • 不可篡改:日志写入后不允许修改,可以通过追加写、哈希链、写入独立的审计系统等方式保证。

  • 可检索:支持按用户、按时间、按接口、按请求 ID 检索,方便安全事件回溯。

  • 脱敏存储:日志中的手机号、身份证号、银行卡号、密码等敏感字段必须脱敏或加密存储。

除此之外,还要防止敏感信息在前端和日志中泄露:

  • 接口返回脱敏:手机号中间四位打码,身份证号只保留前后几位,银行卡号只保留后四位。

  • 日志脱敏:日志打印前对敏感字段做处理,禁止把完整 Token、密码、密钥打出来。

  • 错误信息脱敏:生产环境不要直接把异常堆栈返回给客户端,避免暴露内部结构。

java

public class MaskUtil { public static String maskMobile(String mobile) { if (mobile == null || mobile.length() < 11) { return mobile; } return mobile.substring(0, 3) + "****" + mobile.substring(7); } public static String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 8) { return idCard; } return idCard.substring(0, 4) + "**********" + idCard.substring(idCard.length() - 4); } }

十一、总结:面试时怎么把这套方案讲清楚

回到最初的面试题。当被问到“如何保证 API 接口数据安全”时,不要一上来就罗列技术名词,而是按“从传输到业务”的链路组织回答:

  1. 传输安全:全站 HTTPS,TLS 1.2 及以上,移动端做证书校验,防止中间人窃听和篡改。

  2. 身份认证:根据场景选择 Session、Token、JWT 或 OAuth2.0,配合短有效期、刷新机制和黑名单,降低令牌泄露风险。

  3. 授权控制:RBAC 做接口级权限,资源归属校验做数据级权限,防止垂直越权和水平越权。

  4. 输入校验:预编译防 SQL 注入,输出转义防 XSS,参数白名单和 Bean Validation 防非法输入。

  5. 防篡改与防重放:时间戳 + Nonce + 签名,服务端做验签和 Nonce 去重,保证请求来源可信、内容未改、不被重复利用。

  6. 幂等设计:以业务唯一键做去重,保证同一业务操作重复执行不产生副作用。

  7. 限流与防刷:IP、用户、接口、全局多维度限流,防止接口被恶意刷量打垮。

  8. 审计与脱敏:敏感操作留审计日志,接口返回和日志打印对敏感字段脱敏,错误信息不暴露内部细节。

如果面试官继续追问,可以分别展开:HTTPS 握手过程、JWT 与 Session 的取舍、OAuth2.0 授权码模式的流程、SQL 注入的防御原理、Nonce 的存储和过期策略、签名的规范化字符串拼接方式、幂等的四种实现方式、令牌桶与漏桶的差异、审计日志的存储方案等。能把这些细节说清楚,就说明你不仅会背方案,更理解每一层安全措施背后的原理和工程取舍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询