后端接口的“安全防护”体系:从“裸奔暴露”到“纵深防御”

68 阅读7分钟

后端接口作为系统与外部交互的门户,时刻面临着各种安全威胁——SQL注入、XSS攻击、身份伪造、数据泄露等,一旦防护失效,可能导致用户信息泄露、业务逻辑被篡改甚至系统被入侵。安全防护体系通过“身份认证→权限控制→数据加密→攻击拦截”的多层防御,构建从接入到数据的全链路安全屏障,是系统“抗风险能力”的核心体现。

安全防护的核心目标与威胁模型

为什么需要接口安全?

  • 数据保护:防止用户隐私(如手机号、地址)、敏感业务数据(如订单金额、支付信息)被窃取或篡改
  • 身份验证:确保接口调用者是合法用户或授权服务,避免恶意伪造请求
  • 权限管控:限制用户只能访问其有权限的资源(如普通用户不能查看他人订单)
  • 防攻击:抵御常见的网络攻击(如暴力破解、DoS攻击),保障系统可用性

常见的接口安全威胁

  • 注入攻击:SQL注入(如参数包含' OR 1=1 --)、命令注入(如执行系统命令)
  • 跨站脚本(XSS):注入恶意脚本(如<script>窃取Cookie</script>
  • 身份伪造:盗用Token、Session或账号密码进行非法操作
  • 越权访问:通过修改参数访问他人资源(如/api/orders/123改为/api/orders/456查看其他订单)
  • 数据泄露:接口返回过多敏感信息(如用户表完整字段)

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

1. 基于Token的无状态认证

使用JWT(JSON Web Token)实现跨服务、无状态的身份认证:

// 引入JWT依赖
<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>

// Token工具类
@Component
public class JwtTokenProvider {
    // 密钥(生产环境需加密存储,如配置中心)
    @Value("${jwt.secret}")
    private String secretKey;
    // 过期时间(如2小时)
    @Value("${jwt.expiration}")
    private long expiration;

    // 生成Token
    public String generateToken(Long userId, String username) {
        // 设置Token载荷(包含用户ID、用户名等非敏感信息)
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("userId", userId);
        claims.put("role", "USER"); // 角色信息

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, secretKey) // 签名算法
                .compact();
    }

    // 从Token中获取用户ID
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.get("userId").toString());
    }

    // 验证Token有效性
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // Token过期、签名无效等情况均返回false
            return false;
        }
    }
}

2. 认证拦截器:保护接口访问

通过拦截器验证Token,拒绝未认证请求:

@Component
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 1. 从请求头获取Token(格式:Bearer <token>)
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            sendErrorResponse(response, 401, "未提供认证Token");
            return false;
        }
        String token = authHeader.substring(7);

        // 2. 验证Token
        if (!tokenProvider.validateToken(token)) {
            sendErrorResponse(response, 401, "Token无效或已过期");
            return false;
        }

        // 3. 将用户ID存入请求属性,供后续业务使用
        Long userId = tokenProvider.getUserIdFromToken(token);
        request.setAttribute("userId", userId);
        return true;
    }

    // 发送错误响应
    private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
        response.setStatus(status);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":" + status + ",\"message\":\"" + message + "\"}");
    }
}

// 注册拦截器(指定需要认证的接口)
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private JwtAuthenticationInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**") // 所有API接口需要认证
                .excludePathPatterns("/api/auth/login", "/api/auth/register"); // 登录、注册接口例外
    }
}

权限控制:限制“你能做什么”

基于RBAC(角色基础访问控制)模型,控制用户对资源的操作权限:

// 自定义权限注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    String value(); // 权限标识(如"order:delete")
}

// 权限拦截器
@Component
public class PermissionInterceptor implements HandlerInterceptor {
    @Autowired
    private UserPermissionService permissionService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 1. 从请求属性获取用户ID(认证拦截器已设置)
        Long userId = (Long) request.getAttribute("userId");
        if (userId == null) {
            sendErrorResponse(response, 401, "未认证");
            return false;
        }

        // 2. 判断是否是方法调用
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RequirePermission annotation = handlerMethod.getMethodAnnotation(RequirePermission.class);
            if (annotation != null) {
                // 3. 验证用户是否有指定权限
                String permission = annotation.value();
                if (!permissionService.hasPermission(userId, permission)) {
                    sendErrorResponse(response, 403, "没有权限执行此操作");
                    return false;
                }
            }
        }
        return true;
    }

    // 发送错误响应(同认证拦截器)
    private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
        // ...
    }
}

// 在接口中使用权限注解
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    // 删除订单需要"order:delete"权限
    @DeleteMapping("/{id}")
    @RequirePermission("order:delete")
    public Result deleteOrder(@PathVariable Long id, HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        // 业务逻辑(还需验证订单是否属于该用户,防止越权)
        orderService.deleteOrder(id, userId);
        return Result.success("删除成功");
    }
}

数据安全:保护“敏感信息”

1. 数据加密:传输与存储安全

  • 传输加密:使用HTTPS加密传输数据,防止中间人窃听

    # Spring Boot配置HTTPS
    server:
      port: 443
      ssl:
        key-store: classpath:server.p12 # 证书文件
        key-store-password: 123456 # 证书密码
        key-store-type: PKCS12
        key-alias: server
    
  • 存储加密:敏感字段加密存储(如手机号、身份证号)

    // 加密工具类(使用AES算法)
    public class AesEncryptor {
        private static final String KEY = "1234567890abcdef"; // 密钥(16位)
        private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
    
        // 加密
        public static String encrypt(String content) throws Exception {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        }
    
        // 解密
        public static String decrypt(String encryptedContent) throws Exception {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedContent));
            return new String(decrypted, StandardCharsets.UTF_8);
        }
    }
    
    // 实体类中使用加密存储
    @Entity
    @Table(name = "user")
    public class User {
        @Id
        private Long id;
        private String username;
    
        // 手机号加密存储
        @Column(name = "phone")
        private String encryptedPhone;
    
        // 获取手机号(自动解密)
        public String getPhone() throws Exception {
            return AesEncryptor.decrypt(encryptedPhone);
        }
    
        // 设置手机号(自动加密)
        public void setPhone(String phone) throws Exception {
            this.encryptedPhone = AesEncryptor.encrypt(phone);
        }
    }
    

2. 数据脱敏:展示层保护

接口返回时对敏感信息脱敏(如手机号显示为138****8000):

// 脱敏工具类
public class SensitiveUtils {
    // 手机号脱敏(保留前3后4)
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) {
            return phone;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    // 身份证号脱敏(保留前6后4)
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            return idCard;
        }
        return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
    }
}

// DTO中使用脱敏
@Data
public class UserDTO {
    private Long id;
    private String username;

    // 原始手机号(不返回)
    @JsonIgnore
    private String phone;

    // 脱敏后的手机号(返回给前端)
    @JsonProperty("phone")
    public String getMaskedPhone() {
        return SensitiveUtils.maskPhone(phone);
    }
}

攻击防护:主动拦截恶意请求

1. SQL注入防护

  • 使用参数化查询(MyBatis的#{}, JPA的?占位符),避免字符串拼接SQL

    <!-- 安全:使用#{}参数化 -->
    <select id="getUserById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    <!-- 危险:使用${}会导致SQL注入 -->
    <select id="getUserByBadId" resultType="User">
        SELECT * FROM user WHERE id = ${id} <!-- 若id为"1 OR 1=1",会查询所有用户 -->
    </select>
    
  • 输入验证:限制参数格式(如ID必须为数字)

2. XSS攻击防护

通过过滤器过滤请求中的恶意脚本:

// XSS过滤器(替换<、>等特殊字符)
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
    }
}

// 包装类处理参数
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    // 过滤参数值
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return cleanXss(value);
    }

    // 过滤请求体(JSON参数)
    @Override
    public ServletInputStream getInputStream() throws IOException {
        String body = IOUtils.toString(super.getInputStream(), StandardCharsets.UTF_8);
        String cleanBody = cleanXss(body);
        return new ByteArrayInputStream(cleanBody.getBytes(StandardCharsets.UTF_8));
    }

    // 清理XSS脚本
    private String cleanXss(String value) {
        if (value == null) {
            return null;
        }
        // 替换危险标签和字符
        return value.replaceAll("<", "&lt;")
                   .replaceAll(">", "&gt;")
                   .replaceAll("'", "&#39;")
                   .replaceAll("eval\\((.*)\\)", "")
                   .replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
    }
}

安全防护的最佳实践

1. 安全配置加固

  • 隐藏服务器信息:移除响应头中的ServerX-Powered-By等标识

    @Bean
    public FilterRegistrationBean<HeaderFilter> headerFilter() {
        FilterRegistrationBean<HeaderFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new HeaderFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
    
    public class HeaderFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Server", ""); // 清空服务器标识
            httpResponse.setHeader("X-Content-Type-Options", "nosniff"); // 防止MIME类型嗅探
            httpResponse.setHeader("X-XSS-Protection", "1; mode=block"); // 启用XSS过滤
            chain.doFilter(request, response);
        }
    }
    
  • 限制请求方法和内容类型:仅允许必要的HTTP方法(如GET、POST)

2. 安全审计与漏洞扫描

  • 定期进行安全审计:检查接口是否存在越权、泄露等问题
  • 使用工具扫描漏洞:如OWASP ZAP扫描SQL注入、XSS等漏洞
  • 记录安全日志:记录认证失败、权限拒绝等异常行为,便于追溯攻击

避坑指南

  • 密钥管理要安全:避免硬编码密钥,使用配置中心或加密存储
  • 不要依赖前端验证:前端验证仅提升体验,后端必须重复验证
  • Token过期时间要合理:过短影响用户体验,过长增加被盗风险(建议2小时)
  • 敏感操作需二次验证:如支付、修改密码等操作,需额外验证(如短信验证码)

安全防护的核心是“纵深防御”——多层防护措施相互补充,即使某一层被突破,其他层仍能提供保护。一个完善的安全体系,不是追求“绝对安全”,而是通过合理的防护措施将风险降低到可接受范围,这是后端接口“可靠性”的底线要求。