首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >接口安全攻防战:从入门到精通的全方位防护指南

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

作者头像
果酱带你啃java
发布2026-04-14 13:24:02
发布2026-04-14 13:24:02
420
举报

在当今的 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)实现示例

首先添加必要的依赖:

代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制

JWT 工具类实现:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

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

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

配置拦截器:

代码语言:javascript
复制
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"); // 排除令牌刷新接口
    }
}
代码语言:javascript
复制

认证控制器实现:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

相关数据传输对象:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

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

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

角色权限拦截器:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

在 WebMvcConfig 中注册该拦截器:

代码语言:javascript
复制
// 在WebMvcConfig的addInterceptors方法中添加
registry.addInterceptor(roleAuthInterceptor)
        .addPathPatterns("/api/**");
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
@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) {
    // 实现代码...
}
代码语言:javascript
复制

2.2.2 基于资源的访问控制

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

代码语言:javascript
复制
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));
        }
    }
}
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
@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);
}
代码语言:javascript
复制

2.3 数据传输安全

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

2.3.1 HTTPS 的重要性

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

在 Spring Boot 中配置 HTTPS:

代码语言:javascript
复制
# 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
代码语言:javascript
复制

配置 HTTP 到 HTTPS 的重定向:

代码语言:javascript
复制
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 外,还可以采用请求签名机制,确保请求的完整性和真实性。

签名生成流程:

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

签名验证工具类:

代码语言:javascript
复制
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;
        }
    }
}
代码语言:javascript
复制

签名验证拦截器:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

2.4 防止重放攻击

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

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

实现示例:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

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

代码语言:javascript
复制
// 修改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;
}
代码语言:javascript
复制

三、输入验证与输出编码

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

3.1 输入验证策略

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

3.1.1 请求参数验证

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

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

在控制器中启用验证:

代码语言:javascript
复制
@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);
}
代码语言:javascript
复制

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

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

3.1.2 防 SQL 注入

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

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

安全的 MyBatis 映射文件:

代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制

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

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

3.2 输出编码

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

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

在 DTO 中使用输出编码:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

四、接口安全高级防护

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

4.1 API 限流与熔断

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

代码语言:javascript
复制
# 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
代码语言:javascript
复制

自定义限流键解析器:

代码语言:javascript
复制
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);
        };
    }
}
代码语言:javascript
复制

使用 Resilience4j 实现熔断:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

4.2 敏感数据保护

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

4.2.1 密码加密存储

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

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

在用户服务中使用:

代码语言:javascript
复制
/**
 * 用户注册
 *
 * @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());
}
代码语言:javascript
复制

4.2.2 敏感数据脱敏
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
/**
 * 转换实体为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;
}
代码语言:javascript
复制

4.3 安全监控与审计

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

4.3.1 安全日志记录
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

自定义安全审计注解:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

使用示例:

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

五、接口安全部署与运维

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

5.1 安全配置最佳实践

5.1.1 服务器安全配置
代码语言:javascript
复制
# 禁用不必要的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
代码语言:javascript
复制

配置安全响应头:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

5.2 安全漏洞扫描与修复

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

代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制

运行扫描命令:

代码语言:javascript
复制
mvn org.owasp:dependency-check-maven:check
代码语言:javascript
复制

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

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

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

  1. 身份认证与授权
    • 使用 JWT 等现代令牌机制进行身份认证
    • 实施基于角色和资源的细粒度授权
    • 合理设置令牌有效期,实现安全的令牌刷新机制
  2. 数据传输安全
    • 所有接口强制使用 HTTPS
    • 敏感接口采用请求签名机制
    • 实现防重放攻击措施(时间戳 + Nonce)
  3. 输入验证与输出编码
    • 对所有用户输入进行严格验证,遵循白名单原则
    • 使用参数化查询防止 SQL 注入
    • 对输出数据进行适当编码,防止 XSS 攻击
  4. 限流与熔断
    • 实施接口限流,防止恶意请求和 DoS 攻击
    • 使用熔断机制保护系统,防止级联故障
    • 对敏感操作实施更严格的限流策略
  5. 敏感数据保护
    • 密码等敏感信息使用强哈希算法加密存储
    • 传输和存储敏感数据时进行加密
    • 展示敏感数据时进行脱敏处理
  6. 安全监控与审计
    • 记录关键操作的安全日志
    • 实施实时安全监控,及时发现异常
    • 定期进行安全审计和漏洞扫描
  7. 安全开发生命周期
    • 在开发初期就考虑安全需求
    • 定期进行安全培训,提高开发人员安全意识
    • 建立安全漏洞响应和修复流程
  8. 持续改进
    • 关注最新的安全威胁和漏洞
    • 定期更新安全策略和防护措施
    • 参与安全社区,学习最佳实践

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、接口安全威胁全景图
    • 1.1 常见攻击类型及原理
      • 1.1.1 身份认证绕过
      • 1.1.2 授权缺陷攻击
      • 1.1.3 数据传输安全问题
      • 1.1.4 输入验证不足
      • 1.1.5 滥用与 DoS 攻击
    • 1.2 接口攻击的一般流程
  • 二、接口安全基础防护体系
    • 2.1 身份认证机制
      • 2.1.1 令牌认证(Token-based Authentication)
      • 2.1.2 认证机制的安全最佳实践
    • 2.2 授权控制
      • 2.2.1 基于角色的访问控制(RBAC)
      • 2.2.2 基于资源的访问控制
    • 2.3 数据传输安全
      • 2.3.1 HTTPS 的重要性
    • 2.4 防止重放攻击
  • 三、输入验证与输出编码
    • 3.1 输入验证策略
      • 3.1.1 请求参数验证
      • 3.1.2 防 SQL 注入
    • 3.2 输出编码
  • 四、接口安全高级防护
    • 4.1 API 限流与熔断
    • 4.2 敏感数据保护
      • 4.2.1 密码加密存储
      • 4.2.2 敏感数据脱敏
    • 4.3 安全监控与审计
      • 4.3.1 安全日志记录
  • 五、接口安全部署与运维
    • 5.1 安全配置最佳实践
      • 5.1.1 服务器安全配置
    • 5.2 安全漏洞扫描与修复
  • 六、接口安全最佳实践总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档