
在当今的 API 经济时代,接口已成为系统间通信的桥梁,承载着数据传输、业务交互的重要使命。然而,这个桥梁却时常成为黑客攻击的目标 —— 从简单的参数篡改到复杂的重放攻击,从 SQL 注入到 DDoS 肆虐,接口安全事件频发,轻则导致数据泄露,重则引发系统性瘫痪。
据 OWASP(开放 Web 应用安全项目)2023 年报告显示,API 相关漏洞已跃居 Web 应用安全风险 Top 10 的第二位,较 2021 年上升 3 个名次。更触目惊心的是,78% 的 API 攻击事件会导致敏感数据泄露,平均每起事件造成的损失超过 400 万美元。
作为开发者,我们该如何构建坚实的接口安全防线?本文将带你深入接口安全的核心领域,从常见攻击手段剖析到防御策略落地,从代码实现到架构设计,全方位解读接口安全的奥秘,助你打造固若金汤的 API 防护体系。
接口安全并非单一维度的问题,而是涉及认证、授权、数据传输、输入验证等多个层面的系统工程。在深入防御策略之前,我们首先需要了解接口面临的主要威胁类型及其危害。
攻击者通过伪造身份信息或利用认证机制漏洞,在未获得合法权限的情况下访问受保护的接口。常见手段包括:
即使通过了身份认证,攻击者仍可能通过越权操作访问未授权资源。典型场景有:
/admin/*路径的接口)在数据传输过程中,攻击者可能通过以下方式窃取或篡改数据:
由于对用户输入缺乏严格验证,导致各类注入攻击:
攻击者针对接口的攻击通常遵循一定的规律,了解这一流程有助于我们构建更有针对性的防御体系:

构建接口安全防护体系需要从基础做起,形成多层次、全方位的防御策略。本节将介绍接口安全的核心防护措施,包括身份认证、授权控制、数据加密等关键技术点。
身份认证是接口安全的第一道防线,其核心目标是确保请求者的身份真实有效。
基于令牌的认证是目前最流行的 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 ";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的Authorization
String 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;
@Override
public 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;
}
认证解决了 "你是谁" 的问题,而授权则解决了 "你能做什么" 的问题。有效的授权控制可以防止未授权访问和权限滥用。
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;
@Override
public 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) {
// 实现代码...
}
除了基于角色的控制,有时还需要更精细的基于资源的访问控制,即验证用户是否有权限访问特定资源。
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);
}
数据在传输过程中容易受到窃听、篡改等攻击,因此必须保证传输通道的安全性。
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实例
*/
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected 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实例
*/
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
}2.3.2 请求签名机制
对于敏感接口,除了 HTTPS 外,还可以采用请求签名机制,确保请求的完整性和真实性。
签名生成流程:
签名验证工具类:
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;
@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));
}
// 对于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;
}
}
重放攻击是指攻击者截获并重复发送有效的请求,以达到欺骗服务器的目的。防止重放攻击的常用手段包括:
实现示例:
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() {
// 启动定时任务清理过期的Nonce
startCleanupTask();
}
/**
* 验证时间戳和Nonce,防止重放攻击
*
* @param timestamp 时间戳(毫秒)
* @param nonce 随机字符串
* @return 如果验证通过,则返回true;否则返回false
*/
public boolean validate(long timestamp, String nonce) {
// 验证Nonce
if (!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);
// 清理过期的Nonce
long 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 攻击等。有效的输入验证和输出编码可以大幅降低这些风险。
输入验证应遵循 "白名单" 原则,即只允许已知的合法输入,拒绝所有其他输入。
使用 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.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);
}
除了输入验证,使用参数化查询是防止 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_time
FROM user
WHERE 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_time
FROM user
WHERE 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 用户列表
*/
@Override
public 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);
}
}
输出编码是防止 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("<");
break;
case '>':
sb.append(">");
break;
case '&':
sb.append("&");
break;
case '"':
sb.append(""");
break;
case '\'':
sb.append("'");
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;
}
}
除了基础防护措施,对于高安全性要求的系统,还需要实施更高级的防护策略。
限流可以防止接口被恶意请求淹没,保护服务器资源。Spring Cloud Gateway 提供了强大的限流功能:
# application.yml
spring:
cloud:
gateway:
routes:
- id: api_route
uri: lb://api-service
predicates:
- Path=/api/**filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 令牌桶填充速率(每秒)
redis-rate-limiter.burstCapacity: 20 # 令牌桶总容量
key-resolver: "#{@ipAddressKeyResolver}" # 限流键解析器,使用IP地址
- name: CircuitBreaker
args:
name: apiCircuitBreaker
fallbackUri: 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实例
*/
@Bean
public KeyResolver ipAddressKeyResolver() {
return exchange -> {
// 获取客户端IP地址
String ipAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
return Mono.just(ipAddress);
};
}
/**
* 基于用户的限流键解析器
*
* @return KeyResolver实例
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
// 从请求头中获取用户名
String username = exchange.getRequest().getHeaders().getFirst("X-User-Name");
return Mono.justOrEmpty(username)
.defaultIfEmpty("anonymous"); // 匿名用户
};
}
/**
* 基于接口的限流键解析器
*
* @return KeyResolver实例
*/
@Bean
public 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);
// 调用第三方API
String 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);
}
}
敏感数据(如密码、身份证号、银行卡号等)需要特殊保护,包括传输加密、存储加密和脱敏展示。
使用 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) {
// 生成盐并加密密码,工作因子为12
return 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());
}
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;
}
建立完善的安全监控和审计机制,及时发现和响应安全事件。
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);
}
接口安全不仅是开发阶段的任务,还需要在部署和运维阶段持续关注和改进。
# 禁用不必要的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实例
*/
@Bean
public 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配置,强制使用HTTPS
headers.put("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// 配置响应头写入器
StaticHeadersWriter headersWriter = new StaticHeadersWriter(headers);
return new HeaderWriterFilter(headersWriter);
}
/**
* 处理反向代理环境下的头信息
*
* @return ForwardedHeaderTransformer实例
*/
@Bean
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
}
定期进行安全漏洞扫描是发现和修复安全问题的重要手段。可以集成 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目录下,包含项目依赖中的已知安全漏洞信息。
接口安全是一个持续改进的过程,需要结合业务需求和安全风险,采取多层次的防护措施。以下是接口安全的最佳实践总结:
通过实施这些最佳实践,我们可以构建一个多层次、全方位的接口安全防护体系,有效抵御各种安全威胁,保护系统和用户数据的安全。
接口安全没有一劳永逸的解决方案,需要我们持续关注、不断改进,才能在日益复杂的安全环境中保持系统的安全稳定运行。希望本文介绍的知识和实践能够帮助你构建更安全的 API 系统,为用户提供可靠的服务。