当前位置: 首页 > news >正文

接口安全攻防战:从入门到精通的全方位防护指南

在当今的 API 经济时代,接口已成为系统间通信的桥梁,承载着数据传输、业务交互的重要使命。然而,这个桥梁却时常成为黑客攻击的目标 —— 从简单的参数篡改到复杂的重放攻击,从 SQL 注入到 DDoS 肆虐,接口安全事件频发,轻则导致数据泄露,重则引发系统性瘫痪。

据 OWASP(开放 Web 应用安全项目)2023 年报告显示,API 相关漏洞已跃居 Web 应用安全风险 Top 10 的第二位,较 2021 年上升 3 个名次。更触目惊心的是,78% 的 API 攻击事件会导致敏感数据泄露,平均每起事件造成的损失超过 400 万美元。

作为开发者,我们该如何构建坚实的接口安全防线?本文将带你深入接口安全的核心领域,从常见攻击手段剖析到防御策略落地,从代码实现到架构设计,全方位解读接口安全的奥秘,助你打造固若金汤的 API 防护体系。

一、接口安全威胁全景图

接口安全并非单一维度的问题,而是涉及认证、授权、数据传输、输入验证等多个层面的系统工程。在深入防御策略之前,我们首先需要了解接口面临的主要威胁类型及其危害。

1.1 常见攻击类型及原理

1.1.1 身份认证绕过

攻击者通过伪造身份信息或利用认证机制漏洞,在未获得合法权限的情况下访问受保护的接口。常见手段包括:

  • 直接使用他人泄露的令牌(Token)
  • 利用固定密钥或弱密钥进行身份伪造
  • 破解会话管理机制,复用会话 ID
1.1.2 授权缺陷攻击

即使通过了身份认证,攻击者仍可能通过越权操作访问未授权资源。典型场景有:

  • 水平越权:访问同级别用户的资源(如通过修改用户 ID 查看他人订单)
  • 垂直越权:普通用户访问管理员接口(如访问/admin/*路径的接口)
1.1.3 数据传输安全问题

在数据传输过程中,攻击者可能通过以下方式窃取或篡改数据:

  • 监听未加密的 HTTP 传输(中间人攻击)
  • 篡改请求参数或响应内容
  • 重放已捕获的请求
1.1.4 输入验证不足

由于对用户输入缺乏严格验证,导致各类注入攻击:

  • SQL 注入:通过构造特殊 SQL 片段,非法操作数据库
  • XSS 攻击:注入恶意脚本,窃取 Cookie 或其他敏感信息
  • 命令注入:通过接口参数注入系统命令
1.1.5 滥用与 DoS 攻击
  • 暴力破解:通过大量尝试猜测密码或令牌
  • 批量请求:短时间内发送大量请求,耗尽服务器资源
  • 恶意爬虫:通过接口大量抓取数据,造成数据泄露

1.2 接口攻击的一般流程

攻击者针对接口的攻击通常遵循一定的规律,了解这一流程有助于我们构建更有针对性的防御体系:

二、接口安全基础防护体系

构建接口安全防护体系需要从基础做起,形成多层次、全方位的防御策略。本节将介绍接口安全的核心防护措施,包括身份认证、授权控制、数据加密等关键技术点。

2.1 身份认证机制

身份认证是接口安全的第一道防线,其核心目标是确保请求者的身份真实有效。

2.1.1 令牌认证(Token-based Authentication)

基于令牌的认证是目前最流行的 API 认证方式之一,其流程如下:

JWT(JSON Web Token)实现示例

首先添加必要的依赖:

<!-- pom.xml -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>

JWT 工具类实现:

package com.example.apisecurity.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;/*** JWT工具类,用于生成、解析和验证JWT令牌** @author ken*/
@Component
@Slf4j
public class JwtUtils {/*** JWT签名密钥,应在生产环境使用更安全的方式存储*/@Value("${jwt.secret}")private String secret;/*** JWT过期时间(毫秒),默认2小时*/@Value("${jwt.expiration:7200000}")private long expiration;/*** 生成签名密钥* * @return 签名密钥*/private Key getSigningKey() {// 使用HMAC-SHA512算法,需要至少512位(64字节)的密钥byte[] keyBytes = secret.getBytes();return Keys.hmacShaKeyFor(keyBytes);}/*** 生成JWT令牌** @param username 用户名* @return JWT令牌字符串*/public String generateToken(String username) {return generateToken(username, new HashMap<>());}/*** 生成带有自定义声明的JWT令牌** @param username 用户名* @param claims 自定义声明* @return JWT令牌字符串*/public String generateToken(String username, Map<String, Object> claims) {Date now = new Date();Date expirationDate = new Date(now.getTime() + expiration);log.info("为用户[{}]生成JWT令牌,过期时间:{}", username, expirationDate);return Jwts.builder().setClaims(claims).setSubject(username).setIssuedAt(now).setExpiration(expirationDate).signWith(getSigningKey(), SignatureAlgorithm.HS512).compact();}/*** 从JWT令牌中获取用户名** @param token JWT令牌* @return 用户名*/public String getUsernameFromToken(String token) {return getClaimFromToken(token, Claims::getSubject);}/*** 从JWT令牌中获取指定声明** @param token 令牌* @param claimsResolver 声明解析器* @param <T> 声明类型* @return 声明值*/public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {final Claims claims = getAllClaimsFromToken(token);return claimsResolver.apply(claims);}/*** 从JWT令牌中获取所有声明** @param token JWT令牌* @return 所有声明*/private Claims getAllClaimsFromToken(String token) {if (!StringUtils.hasText(token)) {throw new IllegalArgumentException("JWT令牌不能为空");}return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}/*** 验证JWT令牌是否有效** @param token JWT令牌* @param username 用户名* @return 如果令牌有效且属于指定用户,则返回true;否则返回false*/public boolean validateToken(String token, String username) {final String tokenUsername = getUsernameFromToken(token);return (username.equals(tokenUsername) && !isTokenExpired(token));}/*** 检查JWT令牌是否已过期** @param token JWT令牌* @return 如果令牌已过期,则返回true;否则返回false*/private boolean isTokenExpired(String token) {final Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}/*** 从JWT令牌中获取过期时间** @param token JWT令牌* @return 过期时间*/public Date getExpirationDateFromToken(String token) {return getClaimFromToken(token, Claims::getExpiration);}/*** 验证令牌是否有效(不验证用户名)** @param token JWT令牌* @return 令牌是否有效*/public boolean isValidToken(String token) {try {Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);return !isTokenExpired(token);} catch (SignatureException ex) {log.error("JWT签名验证失败: {}", ex.getMessage());} catch (MalformedJwtException ex) {log.error("JWT格式错误: {}", ex.getMessage());} catch (ExpiredJwtException ex) {log.error("JWT已过期: {}", ex.getMessage());} catch (UnsupportedJwtException ex) {log.error("不支持的JWT令牌: {}", ex.getMessage());} catch (IllegalArgumentException ex) {log.error("JWT claims为空: {}", ex.getMessage());}return false;}
}

Spring Boot 拦截器实现令牌验证:

package com.example.apisecurity.interceptor;import com.example.apisecurity.util.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;/*** JWT认证拦截器,用于验证请求中的JWT令牌** @author ken*/
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthInterceptor implements HandlerInterceptor {private final JwtUtils jwtUtils;/*** 令牌在请求头中的名称*/private static final String AUTHORIZATION_HEADER = "Authorization";/*** Bearer前缀*/private static final String BEARER_PREFIX = "Bearer ";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求头中的AuthorizationString authHeader = request.getHeader(AUTHORIZATION_HEADER);// 检查Authorization头是否存在且以Bearer开头if (!StringUtils.hasText(authHeader) || !authHeader.startsWith(BEARER_PREFIX)) {log.warn("请求[{}]缺少有效的Authorization头", request.getRequestURI());response.setStatus(HttpStatus.UNAUTHORIZED.value());response.getWriter().write("未授权访问:缺少有效的令牌");return false;}// 提取令牌(去除Bearer前缀)String token = authHeader.substring(BEARER_PREFIX.length());// 验证令牌if (!jwtUtils.isValidToken(token)) {log.warn("请求[{}]的JWT令牌无效", request.getRequestURI());response.setStatus(HttpStatus.UNAUTHORIZED.value());response.getWriter().write("未授权访问:无效的令牌");return false;}// 令牌验证通过,将用户名存入请求属性中,供后续处理使用String username = jwtUtils.getUsernameFromToken(token);request.setAttribute("username", username);log.info("用户[{}]通过JWT认证,请求路径:{}", username, request.getRequestURI());return true;}
}

配置拦截器:

package com.example.apisecurity.config;import com.example.apisecurity.interceptor.JwtAuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** Web MVC配置类,用于注册拦截器** @author ken*/
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {private final JwtAuthInterceptor jwtAuthInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册JWT认证拦截器registry.addInterceptor(jwtAuthInterceptor).addPathPatterns("/api/**") // 拦截所有API请求.excludePathPatterns("/api/auth/login") // 排除登录接口.excludePathPatterns("/api/auth/refresh"); // 排除令牌刷新接口}
}

认证控制器实现:

package com.example.apisecurity.controller;import com.alibaba.fastjson2.JSONObject;
import com.example.apisecurity.dto.LoginRequest;
import com.example.apisecurity.dto.LoginResponse;
import com.example.apisecurity.service.UserService;
import com.example.apisecurity.util.JwtUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 认证控制器,处理登录和令牌刷新请求** @author ken*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "认证接口", description = "处理用户登录和令牌刷新")
public class AuthController {private final UserService userService;private final JwtUtils jwtUtils;/*** 用户登录** @param loginRequest 登录请求参数* @return 包含JWT令牌的响应*/@PostMapping("/login")@Operation(summary = "用户登录", description = "验证用户凭据并返回JWT令牌")@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = LoginResponse.class))),@ApiResponse(responseCode = "401", description = "用户名或密码错误")})public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest) {// 验证请求参数if (!StringUtils.hasText(loginRequest.getUsername()) || !StringUtils.hasText(loginRequest.getPassword())) {log.warn("登录请求参数不完整");return ResponseEntity.badRequest().build();}// 验证用户凭据boolean isAuthenticated = userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword());if (!isAuthenticated) {log.warn("用户[{}]登录失败:用户名或密码错误", loginRequest.getUsername());return ResponseEntity.status(401).build();}// 生成JWT令牌String token = jwtUtils.generateToken(loginRequest.getUsername());log.info("用户[{}]登录成功,生成JWT令牌", loginRequest.getUsername());// 构建并返回响应LoginResponse response = new LoginResponse();response.setToken(token);response.setExpiresIn(jwtUtils.getExpirationDateFromToken(token).getTime() - System.currentTimeMillis());return ResponseEntity.ok(response);}/*** 刷新JWT令牌** @param requestBody 包含旧令牌的请求体* @return 包含新令牌的响应*/@PostMapping("/refresh")@Operation(summary = "刷新令牌", description = "使用旧令牌获取新的JWT令牌")@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "令牌刷新成功"),@ApiResponse(responseCode = "401", description = "旧令牌无效")})public ResponseEntity<JSONObject> refreshToken(@RequestBody JSONObject requestBody) {String oldToken = requestBody.getString("token");// 验证旧令牌if (!StringUtils.hasText(oldToken) || !jwtUtils.isValidToken(oldToken)) {log.warn("令牌刷新失败:无效的旧令牌");return ResponseEntity.status(401).build();}// 获取用户名并生成新令牌String username = jwtUtils.getUsernameFromToken(oldToken);String newToken = jwtUtils.generateToken(username);log.info("用户[{}]的JWT令牌已刷新", username);// 构建并返回响应JSONObject response = new JSONObject();response.put("token", newToken);response.put("expiresIn", jwtUtils.getExpirationDateFromToken(newToken).getTime() - System.currentTimeMillis());return ResponseEntity.ok(response);}
}

相关数据传输对象:

package com.example.apisecurity.dto;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;/*** 登录请求DTO** @author ken*/
@Data
@Schema(description = "登录请求参数")
public class LoginRequest {@Schema(description = "用户名", example = "admin")private String username;@Schema(description = "密码", example = "password123")private String password;
}
package com.example.apisecurity.dto;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;/*** 登录响应DTO** @author ken*/
@Data
@Schema(description = "登录响应结果")
public class LoginResponse {@Schema(description = "JWT令牌")private String token;@Schema(description = "令牌过期时间(毫秒)")private long expiresIn;
}
2.1.2 认证机制的安全最佳实践
  1. 令牌安全存储

    • 避免在客户端存储敏感信息(如密码)
    • 使用 HttpOnly 和 Secure 标志保护 Cookie 中的令牌
    • 对于单页应用,考虑使用内存存储而非 localStorage
  2. 令牌生命周期管理

    • 访问令牌(Access Token)设置较短的有效期(如 15-30 分钟)
    • 使用刷新令牌(Refresh Token)获取新的访问令牌
    • 实现令牌吊销机制,支持用户主动登出
  3. 签名与加密

    • 使用强加密算法(如 HS512、RS256)对令牌进行签名
    • 敏感信息传输使用 HTTPS 加密
    • 密钥定期轮换,避免长期使用同一密钥

2.2 授权控制

认证解决了 "你是谁" 的问题,而授权则解决了 "你能做什么" 的问题。有效的授权控制可以防止未授权访问和权限滥用。

2.2.1 基于角色的访问控制(RBAC)

RBAC 是目前最广泛使用的授权模型,其核心思想是将权限与角色关联,用户通过拥有相应的角色获得权限。

package com.example.apisecurity.annotation;import java.lang.annotation.*;/*** 角色权限注解,用于标注接口所需的角色** @author ken*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {/*** 所需角色列表** @return 角色列表*/String[] value();/*** 是否需要所有角色,默认为false(只需拥有其中一个角色)** @return 是否需要所有角色*/boolean requireAll() default false;
}

角色权限拦截器:

package com.example.apisecurity.interceptor;import com.example.apisecurity.annotation.RequireRole;
import com.example.apisecurity.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.Arrays;
import java.util.List;/*** 角色权限拦截器,用于验证用户是否拥有访问接口所需的角色** @author ken*/
@Component
@Slf4j
@RequiredArgsConstructor
public class RoleAuthInterceptor implements HandlerInterceptor {private final UserService userService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 检查处理器是否为HandlerMethod(即是否为控制器方法)if (!(handler instanceof HandlerMethod handlerMethod)) {return true;}// 检查方法是否标注了RequireRole注解RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);if (requireRole == null) {// 没有标注注解,不需要角色验证return true;}// 获取当前用户名(由JwtAuthInterceptor设置)String username = (String) request.getAttribute("username");if (username == null) {log.warn("用户名不存在,无法进行角色验证");response.setStatus(HttpStatus.UNAUTHORIZED.value());return false;}// 获取用户拥有的角色List<String> userRoles = userService.getUserRoles(username);if (userRoles.isEmpty()) {log.warn("用户[{}]没有任何角色,无法访问需要角色权限的接口", username);response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("权限不足:没有所需角色");return false;}// 获取接口所需的角色String[] requiredRoles = requireRole.value();// 检查用户是否拥有所需的角色boolean hasPermission;if (requireRole.requireAll()) {// 需要拥有所有角色hasPermission = Arrays.stream(requiredRoles).allMatch(role -> userRoles.contains(role));} else {// 只需拥有其中一个角色hasPermission = Arrays.stream(requiredRoles).anyMatch(role -> userRoles.contains(role));}if (!hasPermission) {log.warn("用户[{}]缺少所需角色,无法访问接口[{}]", username, handlerMethod.getMethod().getName());response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("权限不足:缺少所需角色");return false;}log.info("用户[{}]角色验证通过,允许访问接口[{}]", username, handlerMethod.getMethod().getName());return true;}
}

在 WebMvcConfig 中注册该拦截器:

// 在WebMvcConfig的addInterceptors方法中添加
registry.addInterceptor(roleAuthInterceptor).addPathPatterns("/api/**");

使用示例:

@GetMapping("/users")
@RequireRole("ADMIN") // 只有ADMIN角色可以访问
@Operation(summary = "获取所有用户", description = "需要ADMIN角色")
public ResponseEntity<List<UserDTO>> getAllUsers() {// 实现代码...
}@PostMapping("/orders")
@RequireRole(value = {"ADMIN", "USER"}, requireAll = false) // ADMIN或USER角色均可访问
@Operation(summary = "创建订单", description = "需要ADMIN或USER角色")
public ResponseEntity<OrderDTO> createOrder(@RequestBody OrderRequest request) {// 实现代码...
}
2.2.2 基于资源的访问控制

除了基于角色的控制,有时还需要更精细的基于资源的访问控制,即验证用户是否有权限访问特定资源。

package com.example.apisecurity.util;import com.example.apisecurity.exception.AccessDeniedException;
import com.example.apisecurity.service.AuthorizationService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;/*** 资源访问控制工具类** @author ken*/
@Component
@RequiredArgsConstructor
public class ResourceAccessControl {private final AuthorizationService authorizationService;/*** 检查用户是否有权限访问指定资源** @param username 用户名* @param resourceType 资源类型(如"order"、"user"等)* @param resourceId 资源ID* @throws AccessDeniedException 如果没有访问权限,则抛出此异常*/public void checkAccess(String username, String resourceType, String resourceId) {boolean hasAccess = authorizationService.hasResourceAccess(username, resourceType, resourceId);if (!hasAccess) {throw new AccessDeniedException(String.format("用户[%s]没有访问资源[%s:%s]的权限", username, resourceType, resourceId));}}
}

使用示例:

@GetMapping("/orders/{orderId}")
@Operation(summary = "获取订单详情", description = "需要订单访问权限")
public ResponseEntity<OrderDTO> getOrderDetail(@PathVariable String orderId,HttpServletRequest request) {// 获取当前用户名String username = (String) request.getAttribute("username");// 检查是否有权限访问该订单resourceAccessControl.checkAccess(username, "order", orderId);// 查询并返回订单详情OrderDTO order = orderService.getOrderById(orderId);return ResponseEntity.ok(order);
}

2.3 数据传输安全

数据在传输过程中容易受到窃听、篡改等攻击,因此必须保证传输通道的安全性。

2.3.1 HTTPS 的重要性

HTTPS 通过 TLS/SSL 协议对 HTTP 传输进行加密,是保护数据传输安全的基础措施。所有接口都应强制使用 HTTPS,避免使用明文传输。

在 Spring Boot 中配置 HTTPS:

# application.properties
# 启用HTTPS
server.ssl.enabled=true
# 密钥存储路径(通常是PKCS12格式的证书)
server.ssl.key-store=classpath:keystore.p12
# 密钥存储密码
server.ssl.key-store-password=changeit
# 密钥别名
server.ssl.key-alias=myalias
# 密钥存储类型
server.ssl.key-store-type=PKCS12
# 强制所有HTTP请求重定向到HTTPS
server.http2.enabled=true

配置 HTTP 到 HTTPS 的重定向:

package com.example.apisecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ForwardedHeaderFilter;/*** HTTPS配置类** @author ken*/
@Configuration
public class HttpsConfig {/*** 配置HTTP到HTTPS的重定向** @return ServletWebServerFactory实例*/@Beanpublic ServletWebServerFactory servletContainer() {TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {@Overrideprotected void postProcessContext(Context context) {SecurityConstraint securityConstraint = new SecurityConstraint();securityConstraint.setUserConstraint("CONFIDENTIAL");SecurityCollection collection = new SecurityCollection();collection.addPattern("/*");securityConstraint.addCollection(collection);context.addConstraint(securityConstraint);}};tomcat.addAdditionalTomcatConnectors(redirectConnector());return tomcat;}/*** 创建HTTP连接器,用于将HTTP请求重定向到HTTPS** @return Connector实例*/private Connector redirectConnector() {Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);connector.setScheme("http");connector.setPort(8080); // HTTP端口connector.setSecure(false);connector.setRedirectPort(8443); // HTTPS端口return connector;}/*** 处理反向代理环境下的HTTPS头信息** @return ForwardedHeaderFilter实例*/@BeanForwardedHeaderFilter forwardedHeaderFilter() {return new ForwardedHeaderFilter();}
}
2.3.2 请求签名机制

对于敏感接口,除了 HTTPS 外,还可以采用请求签名机制,确保请求的完整性和真实性。

签名生成流程:

  1. 将所有请求参数(包括 URL 参数和请求体)按参数名排序
  2. 拼接成 "key=value&key=value" 的字符串
  3. 在字符串末尾添加密钥(secret)
  4. 使用哈希算法(如 SHA256)计算签名
  5. 将签名作为参数或请求头发送

签名验证工具类:

package com.example.apisecurity.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;/*** 请求签名工具类,用于生成和验证请求签名** @author ken*/
@Component
@Slf4j
public class SignatureUtils {/*** 生成请求签名** @param params 请求参数* @param secret 签名密钥* @return 签名字符串*/public String generateSignature(Map<String, String> params, String secret) {if (params == null || params.isEmpty()) {throw new IllegalArgumentException("请求参数不能为空");}if (!StringUtils.hasText(secret)) {throw new IllegalArgumentException("签名密钥不能为空");}// 对参数进行排序SortedMap<String, String> sortedParams = new TreeMap<>(params);// 拼接参数StringBuilder sb = new StringBuilder();for (Map.Entry<String, String> entry : sortedParams.entrySet()) {String key = entry.getKey();String value = entry.getValue();// 跳过签名参数本身和空值if ("signature".equals(key) || !StringUtils.hasText(value)) {continue;}sb.append(key).append("=").append(value).append("&");}// 移除最后一个&if (sb.length() > 0) {sb.setLength(sb.length() - 1);}// 添加密钥sb.append(secret);// 计算SHA256哈希String signature = DigestUtils.sha256DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));log.debug("生成签名,参数串:{},签名:{}", sb.toString(), signature);return signature;}/*** 验证请求签名** @param params 请求参数(包含signature参数)* @param secret 签名密钥* @return 如果签名验证通过,则返回true;否则返回false*/public boolean verifySignature(Map<String, String> params, String secret) {if (params == null || params.isEmpty()) {log.warn("验证签名失败:请求参数为空");return false;}// 获取请求中的签名String requestSignature = params.get("signature");if (!StringUtils.hasText(requestSignature)) {log.warn("验证签名失败:请求中不包含signature参数");return false;}// 生成期望的签名try {String expectedSignature = generateSignature(params, secret);boolean matched = expectedSignature.equalsIgnoreCase(requestSignature);if (!matched) {log.warn("验证签名失败:期望签名[{}],实际签名[{}]", expectedSignature, requestSignature);}return matched;} catch (Exception e) {log.error("验证签名时发生异常", e);return false;}}
}

签名验证拦截器:

package com.example.apisecurity.interceptor;import com.example.apisecurity.util.SignatureUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;/*** 签名验证拦截器,用于验证请求签名** @author ken*/
@Component
@Slf4j
@RequiredArgsConstructor
public class SignatureVerifyInterceptor implements HandlerInterceptor {private final SignatureUtils signatureUtils;/*** 签名密钥,应在生产环境使用更安全的方式存储*/@Value("${api.signature.secret}")private String signatureSecret;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取所有请求参数Map<String, String> params = new HashMap<>();// 获取URL参数Enumeration<String> paramNames = request.getParameterNames();while (paramNames.hasMoreElements()) {String name = paramNames.nextElement();params.put(name, request.getParameter(name));}// 对于POST请求,还需要获取请求体参数// 注意:这里需要特殊处理才能获取请求体,因为请求体只能读取一次// 实际应用中可以使用ContentCachingRequestWrapper包装请求// 验证签名boolean signatureValid = signatureUtils.verifySignature(params, signatureSecret);if (!signatureValid) {log.warn("请求[{}]签名验证失败", request.getRequestURI());response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("签名验证失败");return false;}log.info("请求[{}]签名验证通过", request.getRequestURI());return true;}
}

2.4 防止重放攻击

重放攻击是指攻击者截获并重复发送有效的请求,以达到欺骗服务器的目的。防止重放攻击的常用手段包括:

  1. 时间戳 + 非 ce(Nonce)机制
  2. 请求有效期限制
  3. 已使用 nonce 黑名单

实现示例:

package com.example.apisecurity.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;/*** 重放攻击防护工具类,基于时间戳和Nonce机制** @author ken*/
@Component
@Slf4j
public class ReplayAttackProtection {/*** 已使用的Nonce缓存,用于防止重复使用*/private final Map<String, Long> usedNonces = new ConcurrentHashMap<>();/*** 请求有效期(秒),默认5分钟*/@Value("${api.replay-protection.expire-seconds:300}")private long expireSeconds;/*** 清理过期Nonce的间隔(秒),默认10分钟*/@Value("${api.replay-protection.clean-interval:600}")private long cleanInterval;public ReplayAttackProtection() {// 启动定时任务清理过期的NoncestartCleanupTask();}/*** 验证时间戳和Nonce,防止重放攻击** @param timestamp 时间戳(毫秒)* @param nonce 随机字符串* @return 如果验证通过,则返回true;否则返回false*/public boolean validate(long timestamp, String nonce) {// 验证Nonceif (!StringUtils.hasText(nonce)) {log.warn("Nonce为空,可能存在重放攻击风险");return false;}// 检查Nonce是否已使用if (usedNonces.containsKey(nonce)) {log.warn("Nonce[{}]已被使用,可能存在重放攻击", nonce);return false;}// 验证时间戳long currentTime = System.currentTimeMillis();long timeDiff = Math.abs(currentTime - timestamp);if (timeDiff > expireSeconds * 1000) {log.warn("时间戳过期,当前时间[{}],请求时间[{}],差值[{}ms]",currentTime, timestamp, timeDiff);return false;}// 将Nonce加入已使用集合,设置过期时间usedNonces.put(nonce, currentTime + expireSeconds * 1000);log.debug("Nonce[{}]验证通过,已标记为已使用", nonce);return true;}/*** 启动定时任务清理过期的Nonce*/private void startCleanupTask() {Thread cleanupThread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {try {// 休眠指定时间TimeUnit.SECONDS.sleep(cleanInterval);// 清理过期的Noncelong currentTime = System.currentTimeMillis();int removedCount = 0;for (Map.Entry<String, Long> entry : usedNonces.entrySet()) {if (entry.getValue() < currentTime) {usedNonces.remove(entry.getKey());removedCount++;}}log.info("清理过期Nonce完成,共移除{}个", removedCount);} catch (InterruptedException e) {log.info("Nonce清理线程被中断");Thread.currentThread().interrupt();break;} catch (Exception e) {log.error("Nonce清理任务发生异常", e);}}}, "NonceCleanupThread");cleanupThread.setDaemon(true);cleanupThread.start();log.info("Nonce清理线程已启动,清理间隔:{}秒", cleanInterval);}
}

在签名验证中集成重放攻击防护:

// 修改SignatureVerifyInterceptor的preHandle方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取所有请求参数Map<String, String> params = new HashMap<>();// 获取URL参数Enumeration<String> paramNames = request.getParameterNames();while (paramNames.hasMoreElements()) {String name = paramNames.nextElement();params.put(name, request.getParameter(name));}// 验证时间戳和Nonce,防止重放攻击String timestampStr = params.get("timestamp");String nonce = params.get("nonce");if (!StringUtils.hasText(timestampStr) || !StringUtils.hasText(nonce)) {log.warn("请求缺少timestamp或nonce参数");response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("缺少必要的安全参数");return false;}try {long timestamp = Long.parseLong(timestampStr);if (!replayAttackProtection.validate(timestamp, nonce)) {log.warn("时间戳或Nonce验证失败,可能存在重放攻击");response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("请求已过期或可能存在重放攻击");return false;}} catch (NumberFormatException e) {log.warn("timestamp参数格式错误:{}", timestampStr);response.setStatus(HttpStatus.BAD_REQUEST.value());response.getWriter().write("timestamp参数格式错误");return false;}// 验证签名boolean signatureValid = signatureUtils.verifySignature(params, signatureSecret);if (!signatureValid) {log.warn("请求[{}]签名验证失败", request.getRequestURI());response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("签名验证失败");return false;}log.info("请求[{}]安全验证通过", request.getRequestURI());return true;
}

三、输入验证与输出编码

输入验证不足是导致多数安全漏洞的根源,如 SQL 注入、XSS 攻击等。有效的输入验证和输出编码可以大幅降低这些风险。

3.1 输入验证策略

输入验证应遵循 "白名单" 原则,即只允许已知的合法输入,拒绝所有其他输入。

3.1.1 请求参数验证

使用 Spring Validation 进行请求参数验证:

package com.example.apisecurity.dto;import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;/*** 用户注册请求DTO** @author ken*/
@Data
@Schema(description = "用户注册请求参数")
public class RegisterRequest {@NotBlank(message = "用户名不能为空")@Size(min = 4, max = 20, message = "用户名长度必须在4-20个字符之间")@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")@Schema(description = "用户名", example = "user123", minLength = 4, maxLength = 20)private String username;@NotBlank(message = "密码不能为空")@Size(min = 8, max = 32, message = "密码长度必须在8-32个字符之间")@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=]).*$", message = "密码必须包含字母、数字和特殊字符")@Schema(description = "密码", example = "Passw0rd!")private String password;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")@Schema(description = "邮箱地址", example = "user@example.com")private String email;@NotBlank(message = "手机号不能为空")@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")@Schema(description = "手机号", example = "13800138000")private String phone;
}

在控制器中启用验证:

@PostMapping("/register")
@Operation(summary = "用户注册", description = "新用户注册接口")
public ResponseEntity<UserDTO> register(@Valid @RequestBody RegisterRequest request) {// 验证通过,处理注册逻辑UserDTO user = userService.register(request);return ResponseEntity.status(HttpStatus.CREATED).body(user);
}

全局异常处理器处理验证失败:

package com.example.apisecurity.exception;import com.alibaba.fastjson2.JSONObject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** 全局异常处理器** @author ken*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 处理请求参数验证失败异常** @param ex MethodArgumentNotValidException异常* @return 包含错误信息的响应*/@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public JSONObject handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {Map<String, String> errors = new HashMap<>();// 获取字段验证错误ex.getBindingResult().getAllErrors().forEach(error -> {String fieldName = ((FieldError) error).getField();String errorMessage = error.getDefaultMessage();errors.put(fieldName, errorMessage);});log.warn("请求参数验证失败:{}", errors);JSONObject response = new JSONObject();response.put("code", HttpStatus.BAD_REQUEST.value());response.put("message", "请求参数验证失败");response.put("errors", errors);return response;}/*** 处理方法参数验证失败异常** @param ex ConstraintViolationException异常* @return 包含错误信息的响应*/@ExceptionHandler(ConstraintViolationException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public JSONObject handleConstraintViolation(ConstraintViolationException ex) {Map<String, String> errors = new HashMap<>();// 获取参数验证错误Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();for (ConstraintViolation<?> violation : violations) {String fieldName = violation.getPropertyPath().toString();String errorMessage = violation.getMessage();errors.put(fieldName, errorMessage);}log.warn("方法参数验证失败:{}", errors);JSONObject response = new JSONObject();response.put("code", HttpStatus.BAD_REQUEST.value());response.put("message", "方法参数验证失败");response.put("errors", errors);return response;}// 其他异常处理方法...
}
3.1.2 防 SQL 注入

除了输入验证,使用参数化查询是防止 SQL 注入的关键。MyBatis-Plus 提供了安全的查询方式:

package com.example.apisecurity.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.apisecurity.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;/*** 用户Mapper接口** @author ken*/
@Repository
public interface UserMapper extends BaseMapper<User> {/*** 安全的根据用户名查询用户* 采用参数化查询,防止SQL注入** @param username 用户名* @return 用户信息*/User selectByUsername(@Param("username") String username);/*** 错误示例:使用字符串拼接,存在SQL注入风险* 注意:实际开发中不要这样写!** @param username 用户名* @return 用户信息*/// User selectByUsernameUnsafe(@Param("username") String username);
}

安全的 MyBatis 映射文件:

<!-- UserMapper.xml -->
<?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.example.apisecurity.mapper.UserMapper"><!-- 安全的查询:使用参数绑定 --><select id="selectByUsername" resultType="com.example.apisecurity.entity.User">SELECT id, username, password, email, phone, status, create_time, update_timeFROM userWHERE username = #{username}LIMIT 1</select><!-- 错误示例:使用${}会导致SQL注入 --><!-- <select id="selectByUsernameUnsafe" resultType="com.example.apisecurity.entity.User">SELECT id, username, password, email, phone, status, create_time, update_timeFROM userWHERE username = '${username}'LIMIT 1</select>-->
</mapper>

使用 MyBatis-Plus 的条件构造器进行复杂查询:

package com.example.apisecurity.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.apisecurity.entity.User;
import com.example.apisecurity.mapper.UserMapper;
import com.example.apisecurity.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.List;/*** 用户服务实现类** @author ken*/
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {private final UserMapper userMapper;/*** 根据状态和角色查询用户* 使用MyBatis-Plus的条件构造器,安全可靠** @param status 状态* @param role 角色* @return 用户列表*/@Overridepublic List<User> findUsersByStatusAndRole(Integer status, String role) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("status", status).like("roles", role).orderByDesc("create_time");return userMapper.selectList(queryWrapper);}
}

3.2 输出编码

输出编码是防止 XSS 攻击的重要手段,特别是当接口返回的数据可能被用于网页渲染时。

package com.example.apisecurity.util;import org.springframework.stereotype.Component;/*** XSS防护工具类,用于对输出进行编码** @author ken*/
@Component
public class XssUtils {/*** 对HTML特殊字符进行编码** @param input 输入字符串* @return 编码后的字符串*/public String htmlEncode(String input) {if (input == null) {return null;}StringBuilder sb = new StringBuilder(input.length());for (char c : input.toCharArray()) {switch (c) {case '<':sb.append("&lt;");break;case '>':sb.append("&gt;");break;case '&':sb.append("&amp;");break;case '"':sb.append("&quot;");break;case '\'':sb.append("&#39;");break;default:sb.append(c);}}return sb.toString();}/*** 对JavaScript特殊字符进行编码** @param input 输入字符串* @return 编码后的字符串*/public String javascriptEncode(String input) {if (input == null) {return null;}StringBuilder sb = new StringBuilder();for (char c : input.toCharArray()) {// 对非ASCII字符使用\uXXXX格式编码if (c < 0x20 || c > 0x7E) {sb.append(String.format("\\u%04X", (int) c));} else {// 对特殊字符进行转义switch (c) {case '\\':sb.append("\\\\");break;case '\'':sb.append("\\'");break;case '"':sb.append("\\\"");break;case '<':sb.append("\\x3C");break;case '>':sb.append("\\x3E");break;case '&':sb.append("\\x26");break;default:sb.append(c);}}}return sb.toString();}
}

在 DTO 中使用输出编码:

package com.example.apisecurity.dto;import com.example.apisecurity.util.XssUtils;
import com.example.apisecurity.entity.Comment;
import lombok.Data;import java.time.LocalDateTime;/*** 评论DTO,用于返回评论信息** @author ken*/
@Data
public class CommentDTO {private Long id;private Long userId;private String username;private String content;private LocalDateTime createTime;/*** 从实体对象转换为DTO,并对内容进行XSS编码** @param comment 评论实体* @param xssUtils XSS工具类* @return 评论DTO*/public static CommentDTO fromEntity(Comment comment, XssUtils xssUtils) {CommentDTO dto = new CommentDTO();dto.setId(comment.getId());dto.setUserId(comment.getUserId());dto.setUsername(comment.getUsername());// 对评论内容进行HTML编码,防止XSS攻击dto.setContent(xssUtils.htmlEncode(comment.getContent()));dto.setCreateTime(comment.getCreateTime());return dto;}
}

四、接口安全高级防护

除了基础防护措施,对于高安全性要求的系统,还需要实施更高级的防护策略。

4.1 API 限流与熔断

限流可以防止接口被恶意请求淹没,保护服务器资源。Spring Cloud Gateway 提供了强大的限流功能:

# application.yml
spring:cloud:gateway:routes:- id: api_routeuri: lb://api-servicepredicates:- Path=/api/**filters:- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 10 # 令牌桶填充速率(每秒)redis-rate-limiter.burstCapacity: 20 # 令牌桶总容量key-resolver: "#{@ipAddressKeyResolver}" # 限流键解析器,使用IP地址- name: CircuitBreakerargs:name: apiCircuitBreakerfallbackUri: forward:/fallback/api

自定义限流键解析器:

package com.example.apisecurity.config;import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;import jakarta.servlet.http.HttpServletRequest;/*** 限流配置类** @author ken*/
@Configuration
public class RateLimitConfig {/*** 基于IP地址的限流键解析器** @return KeyResolver实例*/@Beanpublic KeyResolver ipAddressKeyResolver() {return exchange -> {// 获取客户端IP地址String ipAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();return Mono.just(ipAddress);};}/*** 基于用户的限流键解析器** @return KeyResolver实例*/@Beanpublic KeyResolver userKeyResolver() {return exchange -> {// 从请求头中获取用户名String username = exchange.getRequest().getHeaders().getFirst("X-User-Name");return Mono.justOrEmpty(username).defaultIfEmpty("anonymous"); // 匿名用户};}/*** 基于接口的限流键解析器** @return KeyResolver实例*/@Beanpublic KeyResolver apiKeyResolver() {return exchange -> {// 获取请求路径作为限流键String path = exchange.getRequest().getPath().toString();return Mono.just(path);};}
}

使用 Resilience4j 实现熔断:

package com.example.apisecurity.service;import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;/*** 第三方服务调用服务,使用熔断和重试机制** @author ken*/
@Service
@Slf4j
@RequiredArgsConstructor
public class ThirdPartyService {private final RestTemplate restTemplate;/*** 调用第三方支付服务* 使用@CircuitBreaker注解实现熔断* 使用@Retry注解实现重试** @param orderId 订单ID* @param amount 金额* @return 支付结果*/@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentServiceFallback")@Retry(name = "paymentService", fallbackMethod = "paymentServiceRetryFallback")public String callPaymentService(String orderId, double amount) {log.info("调用第三方支付服务,订单ID:{},金额:{}", orderId, amount);// 调用第三方APIString url = String.format("https://api.payment-service.com/pay?orderId=%s&amount=%f", orderId, amount);return restTemplate.getForObject(url, String.class);}/*** 支付服务熔断降级方法** @param orderId 订单ID* @param amount 金额* @param e 异常* @return 降级处理结果*/public String paymentServiceFallback(String orderId, double amount, Exception e) {log.warn("支付服务熔断降级,订单ID:{},异常:{}", orderId, e.getMessage());// 记录失败订单,以便后续处理// orderService.recordFailedPayment(orderId, amount, e.getMessage());return "支付请求已接收,系统将在服务恢复后自动处理,请稍后查询结果";}/*** 支付服务重试降级方法** @param orderId 订单ID* @param amount 金额* @param e 异常* @return 降级处理结果*/public String paymentServiceRetryFallback(String orderId, double amount, Exception e) {log.warn("支付服务重试失败,订单ID:{},异常:{}", orderId, e.getMessage());return paymentServiceFallback(orderId, amount, e);}
}

4.2 敏感数据保护

敏感数据(如密码、身份证号、银行卡号等)需要特殊保护,包括传输加密、存储加密和脱敏展示。

4.2.1 密码加密存储

使用 BCrypt 加密算法存储密码:

package com.example.apisecurity.util;import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Component;/*** 密码加密工具类,使用BCrypt算法** @author ken*/
@Component
public class PasswordEncoder {/*** 加密密码** @param rawPassword 原始密码* @return 加密后的密码*/public String encode(String rawPassword) {// 生成盐并加密密码,工作因子为12return BCrypt.hashpw(rawPassword, BCrypt.gensalt(12));}/*** 验证密码** @param rawPassword 原始密码* @param encodedPassword 加密后的密码* @return 如果密码匹配,则返回true;否则返回false*/public boolean matches(String rawPassword, String encodedPassword) {if (encodedPassword == null || encodedPassword.isEmpty()) {return false;}return BCrypt.checkpw(rawPassword, encodedPassword);}
}

在用户服务中使用:

/*** 用户注册** @param request 注册请求* @return 注册成功的用户信息*/
@Override
public UserDTO register(RegisterRequest request) {// 检查用户名是否已存在User existingUser = userMapper.selectByUsername(request.getUsername());if (existingUser != null) {throw new UserAlreadyExistsException("用户名已存在");}// 创建新用户User user = new User();user.setUsername(request.getUsername());// 加密密码user.setPassword(passwordEncoder.encode(request.getPassword()));user.setEmail(request.getEmail());user.setPhone(request.getPhone());user.setStatus(1); // 1表示正常状态user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());// 保存用户userMapper.insert(user);log.info("用户[{}]注册成功", request.getUsername());// 转换为DTO并返回return UserDTO.fromEntity(user);
}/*** 用户认证** @param username 用户名* @param password 密码* @return 如果认证成功,则返回true;否则返回false*/
@Override
public boolean authenticate(String username, String password) {// 查询用户User user = userMapper.selectByUsername(username);if (user == null) {return false;}// 验证密码return passwordEncoder.matches(password, user.getPassword());
}
4.2.2 敏感数据脱敏
package com.example.apisecurity.util;import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;/*** 数据脱敏工具类** @author ken*/
@Component
public class DataMaskingUtils {/*** 手机号脱敏:保留前3位和后4位,中间用*代替* 示例:13800138000 -> 138****8000** @param phone 手机号* @return 脱敏后的手机号*/public String maskPhone(String phone) {if (!StringUtils.hasText(phone) || phone.length() != 11) {return phone;}return phone.substring(0, 3) + "****" + phone.substring(7);}/*** 邮箱脱敏:隐藏@前的部分字符,保留前2位和域名* 示例:user@example.com -> us**@example.com** @param email 邮箱地址* @return 脱敏后的邮箱地址*/public String maskEmail(String email) {if (!StringUtils.hasText(email) || !email.contains("@")) {return email;}String[] parts = email.split("@");if (parts.length != 2) {return email;}String username = parts[0];String domain = parts[1];if (username.length() <= 2) {return username + "**@" + domain;}return username.substring(0, 2) + "**@" + domain;}/*** 身份证号脱敏:保留前6位和后4位,中间用*代替* 示例:110101199001011234 -> 110101********1234** @param idCard 身份证号* @return 脱敏后的身份证号*/public String maskIdCard(String idCard) {if (!StringUtils.hasText(idCard) || (idCard.length() != 15 && idCard.length() != 18)) {return idCard;}if (idCard.length() == 15) {return idCard.substring(0, 6) + "*****" + idCard.substring(11);} else {return idCard.substring(0, 6) + "********" + idCard.substring(14);}}/*** 银行卡号脱敏:保留前6位和后4位,中间用*代替* 示例:6222021234567890123 -> 622202*********0123** @param bankCard 银行卡号* @return 脱敏后的银行卡号*/public String maskBankCard(String bankCard) {if (!StringUtils.hasText(bankCard) || bankCard.length() < 10) {return bankCard;}return bankCard.substring(0, 6) + "*********" + bankCard.substring(bankCard.length() - 4);}
}

使用示例:

/*** 转换实体为DTO,并对敏感信息进行脱敏** @param user 用户实体* @return 用户DTO*/
public static UserDTO fromEntity(User user) {UserDTO dto = new UserDTO();dto.setId(user.getId());dto.setUsername(user.getUsername());// 对手机号进行脱敏dto.setPhone(dataMaskingUtils.maskPhone(user.getPhone()));// 对邮箱进行脱敏dto.setEmail(dataMaskingUtils.maskEmail(user.getEmail()));dto.setStatus(user.getStatus());dto.setCreateTime(user.getCreateTime());return dto;
}

4.3 安全监控与审计

建立完善的安全监控和审计机制,及时发现和响应安全事件。

4.3.1 安全日志记录
package com.example.apisecurity.aspect;import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.UUID;/*** 安全审计切面,用于记录敏感操作日志** @author ken*/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityAuditAspect {/*** 定义切点:所有控制器中的敏感操作方法*/@Pointcut("@annotation(com.example.apisecurity.annotation.SecurityAudit)")public void securityAuditPointcut() {}/*** 方法执行前记录日志** @param joinPoint 连接点*/@Before("securityAuditPointcut() && @annotation(auditAnnotation)")public void beforeMethodExecution(JoinPoint joinPoint, SecurityAudit auditAnnotation) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return;}HttpServletRequest request = attributes.getRequest();String requestId = UUID.randomUUID().toString();request.setAttribute("auditRequestId", requestId);// 获取用户名String username = (String) request.getAttribute("username");if (username == null) {username = "anonymous";}// 记录操作开始日志log.info("[安全审计][{}]操作开始 - 操作类型: {}, 用户: {}, IP: {}, 接口: {}, 参数: {}",requestId,auditAnnotation.operation(),username,request.getRemoteAddr(),request.getRequestURI(),Arrays.toString(joinPoint.getArgs()));}/*** 方法执行成功后记录日志** @param joinPoint 连接点* @param result 方法返回结果*/@AfterReturning(pointcut = "securityAuditPointcut()", returning = "result")public void afterMethodSuccess(JoinPoint joinPoint, Object result) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return;}HttpServletRequest request = attributes.getRequest();String requestId = (String) request.getAttribute("auditRequestId");if (requestId == null) {requestId = "unknown";}// 记录操作成功日志log.info("[安全审计][{}]操作成功 - 耗时: {}ms, 结果: {}",requestId,System.currentTimeMillis() - (Long) request.getAttribute("startTime"),result);}/*** 方法执行异常时记录日志** @param joinPoint 连接点* @param ex 异常*/@AfterThrowing(pointcut = "securityAuditPointcut()", throwing = "ex")public void afterMethodException(JoinPoint joinPoint, Exception ex) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return;}HttpServletRequest request = attributes.getRequest();String requestId = (String) request.getAttribute("auditRequestId");if (requestId == null) {requestId = "unknown";}// 记录操作异常日志log.error("[安全审计][{}]操作异常 - 耗时: {}ms, 异常: {}",requestId,System.currentTimeMillis() - (Long) request.getAttribute("startTime"),ex.getMessage(),ex);}
}

自定义安全审计注解:

package com.example.apisecurity.annotation;import java.lang.annotation.*;/*** 安全审计注解,用于标记需要进行安全审计的方法** @author ken*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SecurityAudit {/*** 操作名称** @return 操作名称*/String operation();/*** 是否记录请求参数** @return 是否记录请求参数*/boolean logParameters() default true;/*** 是否记录返回结果** @return 是否记录返回结果*/boolean logResult() default true;
}

使用示例:

@PostMapping("/withdraw")
@SecurityAudit(operation = "用户提现")
@Operation(summary = "用户提现", description = "用户发起提现请求")
public ResponseEntity<WithdrawResponse> withdraw(@Valid @RequestBody WithdrawRequest request) {// 处理提现逻辑WithdrawResponse response = accountService.withdraw(request);return ResponseEntity.ok(response);
}

五、接口安全部署与运维

接口安全不仅是开发阶段的任务,还需要在部署和运维阶段持续关注和改进。

5.1 安全配置最佳实践

5.1.1 服务器安全配置
# 禁用不必要的HTTP方法
server.tomcat.additional-tld-skip-patterns=*.jar
server.tomcat.remoteip.protocol-header=x-forwarded-proto# 安全相关的HTTP响应头
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %D# 会话管理
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=strict
server.servlet.session.timeout=30m

配置安全响应头:

package com.example.apisecurity.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter;
import org.springframework.web.filter.HeaderWriterFilter;
import org.springframework.web.filter.ServerHttpObservationFilter;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.HashMap;
import java.util.Map;/*** 安全响应头配置** @author ken*/
@Configuration
public class SecurityHeaderConfig implements WebMvcConfigurer {/*** 配置安全相关的HTTP响应头** @return HeaderWriterFilter实例*/@Beanpublic HeaderWriterFilter securityHeadersFilter() {Map<String, String> headers = new HashMap<>();// 防止XSS攻击headers.put("X-XSS-Protection", "1; mode=block");// 防止点击劫持headers.put("X-Frame-Options", "DENY");// 防止MIME类型嗅探headers.put("X-Content-Type-Options", "nosniff");// 内容安全策略headers.put("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");// Referrer策略headers.put("Referrer-Policy", "strict-origin-when-cross-origin");// 禁止浏览器自动填充headers.put("X-WebKit-CSP", "default-src 'self'");// HSTS配置,强制使用HTTPSheaders.put("Strict-Transport-Security", "max-age=31536000; includeSubDomains");// 配置响应头写入器StaticHeadersWriter headersWriter = new StaticHeadersWriter(headers);return new HeaderWriterFilter(headersWriter);}/*** 处理反向代理环境下的头信息** @return ForwardedHeaderTransformer实例*/@Beanpublic ForwardedHeaderTransformer forwardedHeaderTransformer() {return new ForwardedHeaderTransformer();}
}

5.2 安全漏洞扫描与修复

定期进行安全漏洞扫描是发现和修复安全问题的重要手段。可以集成 OWASP Dependency-Check 进行依赖包漏洞扫描:

<!-- pom.xml -->
<plugin><groupId>org.owasp</groupId><artifactId>dependency-check-maven</artifactId><version>8.4.0</version><executions><execution><goals><goal>check</goal></goals></execution></executions><configuration><format>HTML</format><outputDirectory>${project.build.directory}/dependency-check-report</outputDirectory><failBuildOnCVSS>7</failBuildOnCVSS> <!-- CVSS评分大于等于7时构建失败 --></configuration>
</plugin>

运行扫描命令:

mvn org.owasp:dependency-check-maven:check

扫描报告将生成在target/dependency-check-report目录下,包含项目依赖中的已知安全漏洞信息。

六、接口安全最佳实践总结

接口安全是一个持续改进的过程,需要结合业务需求和安全风险,采取多层次的防护措施。以下是接口安全的最佳实践总结:

  1. 身份认证与授权

    • 使用 JWT 等现代令牌机制进行身份认证
    • 实施基于角色和资源的细粒度授权
    • 合理设置令牌有效期,实现安全的令牌刷新机制
  2. 数据传输安全

    • 所有接口强制使用 HTTPS
    • 敏感接口采用请求签名机制
    • 实现防重放攻击措施(时间戳 + Nonce)
  3. 输入验证与输出编码

    • 对所有用户输入进行严格验证,遵循白名单原则
    • 使用参数化查询防止 SQL 注入
    • 对输出数据进行适当编码,防止 XSS 攻击
  4. 限流与熔断

    • 实施接口限流,防止恶意请求和 DoS 攻击
    • 使用熔断机制保护系统,防止级联故障
    • 对敏感操作实施更严格的限流策略
  5. 敏感数据保护

    • 密码等敏感信息使用强哈希算法加密存储
    • 传输和存储敏感数据时进行加密
    • 展示敏感数据时进行脱敏处理
  6. 安全监控与审计

    • 记录关键操作的安全日志
    • 实施实时安全监控,及时发现异常
    • 定期进行安全审计和漏洞扫描
  7. 安全开发生命周期

    • 在开发初期就考虑安全需求
    • 定期进行安全培训,提高开发人员安全意识
    • 建立安全漏洞响应和修复流程
  8. 持续改进

    • 关注最新的安全威胁和漏洞
    • 定期更新安全策略和防护措施
    • 参与安全社区,学习最佳实践

通过实施这些最佳实践,我们可以构建一个多层次、全方位的接口安全防护体系,有效抵御各种安全威胁,保护系统和用户数据的安全。

接口安全没有一劳永逸的解决方案,需要我们持续关注、不断改进,才能在日益复杂的安全环境中保持系统的安全稳定运行。希望本文介绍的知识和实践能够帮助你构建更安全的 API 系统,为用户提供可靠的服务。

http://www.dtcms.com/a/391636.html

相关文章:

  • GEO(Generative Engine Optimization)技术详解与2025实践指南
  • Amazon SES 移出沙盒完整指南 高通过率模板
  • 从 IP over 鸽子到 TCP 的适应性
  • 大模型提示工程
  • 鸿蒙应用开发——Repeat组件的使用
  • 远程控制的全球节点功能如何开启?插件类型、并发数量怎么选?
  • 因果推断:因果回归树处理异质性处理效应(三)
  • 六应用层-真题
  • python笔记之正则篇(四)
  • 无标题文档
  • LeetCode 面试经典 150 题之验证回文串:双指针解题思路详解
  • pandas在AI中与其他库的协作
  • 【软件测试】第5章 测试分类(下)
  • 二物理层-真题-
  • c康复训练 01
  • MLP和CNN在图片识别中的对比——基于猫狗分类项目的实战分析
  • Node-Choice
  • PyQt6之滚动条
  • 使用OpenVINO将PP-OCRv5模型部署在Intel显卡上
  • 【图像处理基石】图像复原方面有哪些经典算法?
  • setTimeout定时器不生效- useRef 的特点/作用
  • 钻井的 “导航仪”:一文读懂单点、多点与随钻测量
  • CKS-CN 考试知识点分享(8) ingress 公开 https 服务
  • ​​[硬件电路-259]:LM4040AIM3 精密电压基准源: 管脚定义、概述、功能、技术指标、使用场景、原理
  • C语言:实现阶乘和计算
  • 鸿蒙应用开发——AppStorageV2和PersistenceV2的使用
  • shell脚本实现docker镜像批量保存并上传至Harbor仓库
  • 用 EzCaptcha 优化 reCAPTCHA 低通过率问题
  • 在docker中构建Vue项目
  • 力扣1895. 最大的幻方