服务端安全开发注意事项

不可信数据校验

跨边界(用户输入、网络请求等)传入的不可信数据需在边界内(如服务端)校验。参数校验原则如下:

1、分页参数校验

  • 必须对页码(page)和页大小(size)进行范围与合理性校验。

  • 防止恶意传入极大值(如 size=100000)导致数据库负载过高或内存溢出,需设置允许的最大值。

2、数组 /列表大小校验

  • 对入参中的数组或列表长度进行明确限制。

  • 防止传入超大数组(如十万个ID)导致循环处理耗时过长、数据库查询压力剧增或内存溢出。

3、枚举值范围校验

  • 对于类型、状态等限定范围的字段,必须校验传入值是否在预定义的枚举集合内。

  • 例如,性别字段只允许"男"、"女"或对应的数字枚举值,防止非法参数导致业务逻辑异常。

4、文件上传校验

  • 大小限制:校验单个文件及批量上传总大小,防止磁盘空间被占满或带宽耗尽。

  • 类型限制:通过文件后缀和MIME类型白名单校验,防止上传可执行文件等危险类型。

  • 频率限制:对同一用户或IP的上传频率进行限流,防止恶意刷资源。

防止 SQL 注入

Mybatis 本身是基于 JDBC 封装的。#{para} 是预编译处理(PreparedStatement)范畴的。{para} 是字符串替换。Mybatis 在处理#时,会调用 PreparedStatement 的 set 系列方法来赋值;处理时,就是把 ${para} 替换成变量的值。使用 #{para} 可以有效的防止 SQL 注入,提高系统安全性。

在 mybatis 中,有一些 SQL 语句,需要直接在 SQL 语句中插入一个不改变的字符串,这时如果直接使用 #{},会导致 SQL 语句失效,出现语法错误,主要是有以下三种场景:

1、动态表名/列名

如果 SQL 语句中要实现动态调用表名或者列名,就不能使用预编译了。

2、使用order by

如果 SQL 语句中有 order by,直接使用 #{},会导致 SQL 报错。这时,需要使用 ${} 代替 #{}。

3、动态 SQL 片段

需要从配置文件、数据库或其它来源加载一个完整的、可信任的 SQL 片段并嵌入到主 SQL 中。

敏感信息处理

所有涉及用户个人敏感数据的字段,在存储、传输、使用、日志和前端展示时,都必须进行严格的脱敏或访问控制,总体来看如下场景需要注意:

  • 代码中、配置中心针对敏感信息需要加密处理,如数据库链接的秘钥,访问三方的token等。

  • 数据库、缓存针对敏感信息需要加密存储,如电话、身份证需要加密存储。

  • 用户前端、管理后台针对敏感信息,需要返回脱敏数据,查看明文需要授权和审计。

  • 敏感字段打印日志需脱敏打印,比如电话打印:158****5678

禁止 空指针 引用

直接使用可能为 null 的对象导致 NullPointerException,引发服务异常。

错误场景

java 复制代码
// 高危操作示例 
obj.method(); // obj可能为null 
array.length; // array可能为null 
list.get(i).trim(); // list.get(i)可能为null

解决方案一:前置判空

java 复制代码
if (obj != null) { obj.method(); }

解决方案二:Optional 包装

java 复制代码
Optional.ofNullable(obj).ifPresent(Object::method);

解决方案三:Objects.requireNonNull

java 复制代码
Objects.requireNonNull(obj, "Object cannot be null");

断言禁止副作用

在断言 assert语句中执行具有副作用的操作(如修改对象状态、调用变更方法等)存在显著风险。因为断言可在运行时被全局禁用,届时其中的所有代码将不会执行,导致程序在启用和禁用断言时产生不一致的行为

断言应严格限于验证假设条件必须禁止在其中执行任何会改变程序状态的逻辑。

错误示例:

java 复制代码
// 副作用:删除元素 禁用断言后,删除操作不执行! 
assert names.remove(null);

正确示例:

java 复制代码
// 副作用移至外部 
boolean removed = names.remove(null); 
// 断言仅做布尔检查 
assert removed;

不要扩大重写方法权限

子类重写父类方法时扩大访问权限(如:protected → public),破坏封装性,暴露敏感操作。

错误示例:

java 复制代码
class Parent { 
    protected void doSensitive() { ... } // 预期受限访问 
} 
class Child extends Parent { 
    public void doSensitive() { ... } // 危险:扩大为public 
}

正确示例:

java 复制代码
class Child extends Parent { 
    protected void doSensitive() { ... } // 保持与父类相同权限 
}

设计原则

  • 优先用 final 禁止重写。

  • 重写时严格遵守父类访问修饰符。

不要忽略方法返回值

忽略关键操作的返回值(如文件删除、资源释放的结果)将导致程序无法感知错误,从而埋下隐蔽的缺陷。若依赖方法返回值来判断调用是否成功,或需通过返回值更新本地状态,却忽略该返回值或未对失败情况进行处理,则可能引发程序行为异常、资源泄漏甚至安全风险。因此,必须对关键操作的返回值进行显式检查与妥善应对。

错误示例:

java 复制代码
public void deleteFile() { 
    File someFile = new File("someFileName.txt"); // Do something with someFile           
    someFile.delete(); 
}

上面的错误代码中,删除文件操作没有判断文件是否成功被删除。

正确示例:

java 复制代码
public void deleteFile() { 
    File someFile = new File("someFileName.txt"); 
    // Do something with someFile 
    if (!someFile.delete()) { 
        // Handle failure to delete the file 
    } 
}

该正确示例,判断文件删除是否失败,文件删除失败会有对应的处理。

除数不能为零

除法(/)或取模(%)运算的除数为零时,会抛出 ArithmeticException,导致程序崩溃或服务中断。

错误场景:

java 复制代码
int a = 100; 
int b = 0; // 抛出 ArithmeticException: / by zero 
int result = a / b; 

上面的示例中,有符号操作数num1和num2的除法运算,num2可能为0,导致除0错误的发生。

解决方案:

java 复制代码
if (b == 0) { 
    throw new ArithmeticException("除数不能为零"); 
} else { 
    return a / b; 
}

避免整数溢出

整数运算超出类型范围(如 int 范围 -2³¹ ~ 2³¹-1)会导致静默溢出,引发逻辑错误或安全漏洞(如绕过文件大小检查)。

危险场景

java 复制代码
int max = Integer.MAX_VALUE; 
int overflow = max + 1; // 实际结果:-2147483648(溢出为最小值)

解决方案

java 复制代码
// ✅ 手动检查(通用方案)
static int safeAdd(int a, int b) { 
    if (b > 0 ? a > Integer.MAX_VALUE - b: a < Integer.MIN_VALUE - b) { 
        throw new ArithmeticException("整数溢出"); 
    } else { 
        return a + b; 
    } 
} 

// ✅ Java 8+ 推荐方案 
int result = Math.addExact(a, Math.multiplyExact(b, c));

防止异常泄露敏感信息

服务端抛出的异常信息中可能包含文件路径、数据库结构、资源配置等系统内部敏感细节,若直接将其传递给前端,可能为攻击者提供路径爆破、DoS攻击等关键线索,从而增加系统安全风险。

所以服务中应该明令禁止将原始异常或完整堆栈信息透传给前端,应在服务端对异常进行统一捕获、脱敏或转换后,再向客户端返回无敏感信息的友好提示。

错误做法

java 复制代码
// 直接暴露文件路径和系统环境变量 
public static void main(String[] args) throws FileNotFoundException {            
    FileInputStream fis = new FileInputStream(System.getenv("APPDATA") + args[0]); 
}

此错误示例代码中,当请求的文件不存在时,FileInputStream 的构造器会抛出 FileNotFoundException 异常。这使得攻击者可以不断传入伪造的路径名称来重现出底层文件系统结构。

解决方案一:吞掉异常

java 复制代码
catch (IOException e) { 
    // 吞掉异常 
    log.error("Operation failed"); 
}

解决方案二:统一异常处理

以下只是一个示范,具体抛出什么异常,业务侧决定。

java 复制代码
@ControllerAdvicepublic 
class SecurityExceptionHandler { 
    @ExceptionHandler(Exception.class) 
    public ResponseEntity<ErrorResponse> handle(Exception e) { 
        return ResponseEntity.internalServerError().body(new ErrorResponse("Service unavailable")); 
    } 
}

避免直接捕获 NPE 异常

在程序运行期抛出 NullPointException异常,表明存在对空指针的解引用操作。这类问题应在代码层面予以预防和解决 ,而非通过捕获异常来处理。禁止 通过捕获 NullPointException来规避空指针问题,主要原因如下:

  • 性能代价高昂:与简单的空值检查相比,捕获异常会带来巨大的性能开销。

  • 问题定位困难 :当 try块内有多个表达式可能抛出此异常时,无法准确判断异常的实际抛出位置,不利于问题排查。

  • 程序状态异常 :抛出 NullPointException时,程序通常已处于非正常状态,继续执行可能导致数据不一致或更严重的错误。

正确处理方式是在编码时进行显式的空值检查或使用对象前确保其已完成正确初始化。

错误做法

java 复制代码
boolean isName(String s) { 
    try { 
        String names[] = s.split(" "); 
        if (names.length != 2) { 
            return false; 
        } 
        return (isCapitalized(names[0]) && isCapitalized(names[1])); 
    } catch (NullPointerException e) { 
        return false; 
    } 
}

该错误示例中,isName()接收一个String字符串并在字符串是有效时返回true。该方法中没有主动判断参数String 是否为null,而是通过捕获NullPointException的方式处理。

解决方案

java 复制代码
public boolean validateInput(String s) { 
    // 提前拦截空值 
    if (s == null) return false; 
    String[] parts = s.split(" "); 
    return parts.length == 2 && isValidFormat(parts[0], parts[1]); 
}

优雅方案(Java 8+)

java 复制代码
Optional.ofNullable(input) 
    .map(s -> s.split(" ")) 
    .filter(parts -> parts.length == 2) 
    .map(parts -> isValidFormat(parts[0], parts[1])) 
    .orElse(false);

使用安全随机数

Random 类说明:

java.util.Random是一个可移植、可重现的伪随机数生成器 。若使用相同种子创建两个实例,它们将生成完全相同的数字序列。其种子通常源于系统当前时间,或是在程序重启时被重复使用。因此,攻击者可能通过预测或推算种子值来推断后续随机数序列。该类绝对不可用于安全敏感场景 (如密钥、令牌生成),此类场景应使用 java.security.SecureRandom

SecureRandom 类说明:

java.security.SecureRandom是一个专为密码学安全 设计的伪随机数生成器。它通过高熵源(如硬件随机事件)生成种子,确保输出的随机数序列高度不可预测 ,可抵御恶意推测。虽然在熵不足时可能产生延迟,但这是为保障安全性而进行的必要等待。所有涉及安全、隐私或资源控制的随机数生成,都必须使用此类。

java 复制代码
/** 
* 获得一个SecureRandom 实例 
*/ 
public static SecureRandom getInstance() throws NoSuchAlgorithmException { 
    return isWindows() ? SecureRandom.getInstanceStrong() :             
    SecureRandom.getInstance("NativePRNGNonBlocking"); 
} 

/** 
* 判断当前服务是否运行在window系统上 
*/ 
public static boolean isWindows() { 
    return System.getProperty("os.name").toUpperCase(Locale.ENGLISH).contains("WINDOWS"); 
}

加密算法使用建议

禁止使用私有算法或者弱加密算法(比如DES,SHA1等),应该使用经过验证的、安全的、公开的加密算法。

  • 禁止使用 MD5、SHA1、ECB、DES 等不安全的加密算法。

  • 如果使用了 MD5 算法建议替换成 HMAC_SHA256 算法。

  • 如果使用了 DES 算法建议替换成 AES 算法,AES 算法推荐使用 AES/GCM/NoPadding 模式。

  • PBKDF2 算法建议使用 SHA256 及以上算法, 比如:PBKDF2WithHmacSHA256。

  • SHA256 算法进行签名推荐采用 PSS 填充方式,比如:SHA256withRSA/PSS。

  • RSA 算法进行签名验签的时候,推荐使用 RSA/None/OAEPWithSHA-256AndMGF1Padding 模式。

业务权限校验

横向越权

  • 横向越权指的是攻击者尝试访问与他拥有相同权限的用户的资源。

  • 常见校验:token校验、token和用户id校验、用户id和绑定资源id校验。

纵向越权

  • 纵向越权指的是一个低级别攻击者尝试访问高级别用户的资源。

  • 常见校验:普通用户通过修改请求参数,可以获取管理员的审批或者查阅权限。

IO流资源关闭

如果IO资源不再被使用了,需要我关闭,否则很容易造成资源的泄露。以下介绍两种资源关闭方式:

方案一: finally 主动关闭资源

java 复制代码
import java.io.BufferedReader; 
import java.io.FileReader; 
import java.io.IOException; 
public class TraditionalExample { 
    public static void main(String[] args) { 
        BufferedReader br = null; 
        try { br = new BufferedReader(new FileReader("example.txt")); 
            String line; 
            while ((line = br.readLine()) != null) { 
                System.out.println(line); 
            } 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } finally { 
            if (br != null) { 
                try { 
                    br.close(); 
                } catch (IOException e) { 
                    e.printStackTrace(); 
                } 
            } 
        } 
    } 
}

方案二:使用 try-with-resources 机制,自动关闭资源

java 复制代码
import java.io.BufferedReader; 
import java.io.FileReader; 
import java.io.IOException; 
public class TryWithResourcesExample { 
    public static void main(String[] args) { 
    // 使用 try-with-resources 来管理 BufferedReader 和 FileReader 资源 
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {     
            String line; 
            while ((line = br.readLine()) != null) { 
                System.out.println(line); 
            } 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 

}

内存流

  • 内存流是用于在内存中操作数据的流。它不涉及外部资源(如文件或网络)不需要主动关闭。

  • ByteArrayInputStreamByteArrayOutputStream:用于操作字节数组。

  • StringBufferInputStreamStringWriter:用于操作字符串。

文件流

  • 文件流在使用后必须显式关闭,以释放文件句柄和磁盘资源。如果未关闭,可能导致文件被锁定或资源泄漏。

  • FileInputStreamFileOutputStream:用于操作字节文件。

  • BufferedReaderBufferedWriter:用于操作字符文件(通常与 FileReaderFileWriter 结合使用)。

网络流

  • 网络流在使用后必须显式关闭,以释放网络资源和套接字连接。未关闭可能导致网络资源泄漏或连接阻塞。

  • SocketServerSocket:用于 TCP 通信。

  • DatagramSocketDatagramPacket:用于 UDP 通信。

  • InputStreamOutputStream:通常与套接字结合使用。

相关推荐
bigcarp2 小时前
Roundcube Webmail + sqlite
数据库·sqlite
m0_662577972 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
_Re.2 小时前
达梦数据库阻塞及锁处理
数据库
于慨2 小时前
spring boot
java·数据库·spring boot
爱学习的小可爱卢2 小时前
Redis从入门到精通:入门到精通(万字详解)
数据库·redis·中间件
sqyno1sky2 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
爱丽_2 小时前
G1 深入:Region、Remembered Set、三色标记与“可预测停顿”
java·数据库·算法
dapeng28702 小时前
机器学习与人工智能
jvm·数据库·python
洛兮银儿2 小时前
如何用python连接mysql数据库?
数据库·mysql