SpringBoot框架凭借其自动配置和快速启动的特性,已经成为企业级应用开发的事实标准。但便利性往往伴随着安全风险的隐蔽性,很多开发者直到生产环境被攻破,才意识到那些默认开启的端点、未过滤的输入和混乱的权限配置有多么致命。安全加固不是事后补救,而是开发阶段就必须嵌入代码的肌肉记忆。下面直接进入具体的实操环节,不谈理论空话,只讲怎么改代码、怎么配参数。

彻底关闭或严控Actuator端点暴露

SpringBoot Actuator是信息泄露的重灾区。生产环境中,绝对不能保留/env、/heapdump、/threaddump等端点的完全开放。最稳妥的做法不是仅仅在配置里改几个通配符,而是在引入依赖时就做减法。如果你不需要自定义健康检查之外的任何监控,直接在pom.xml中去掉spring-boot-starter-actuator依赖。如果必须保留,那么要在application.yml中采用白名单机制,只暴露你明确需要的端点。

management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: never
    env:
      enabled: false

注意,仅仅隐藏端点还不够,必须为Actuator单独分配一个非标准端口,并且这个端口不能被公网直接访问,只能通过内网监控系统拉取。同时,为这个端口配置独立的防火墙规则,并在Spring Security中为Actuator路径设置独立的认证角色,不要和业务接口共用同一个权限域。

全局关闭默认错误页面中的堆栈泄露

SpringBoot默认的/error路径在遇到异常时,会把完整的堆栈轨迹、服务版本甚至部分代码片段返回给前端。这在调试时很方便,但上线后就是送给攻击者的情报。你需要覆盖默认的错误处理,返回统一且不含任何技术细节的JSON结构。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity> handleException(Exception e) {
        Map body = new HashMap<>();
        body.put("code", 500);
        body.put("message", "服务器内部错误,请稍后重试");
        // 绝对不要把 e.getMessage() 或 e.getStackTrace() 塞进返回体
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
    }
}

同时,在配置文件中显式关闭Whitelabel Error Page和堆栈输出。

server:
  error:
    whitelabel:
      enabled: false
    include-stacktrace: never
    include-exception: false
强制HTTPS并实施严格的TLS协议控制

在云原生环境下,证书往往在网关层卸载,但如果你需要在SpringBoot层面直接处理HTTPS,就不能只配一个证书文件就完事。必须在内嵌容器的配置中,显式禁用所有老旧和不安全的TLS版本,并限制加密套件,避免降级攻击。

server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: your-strong-password
    key-alias: tomcat
    protocol: TLS
    enabled-protocols: TLSv1.2,TLSv1.3
    ciphers: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

同时,在应用层面添加一个拦截器或过滤器,检查请求是否通过HTTPS进入,如果因为网关配置失误导致HTTP流量直接到达应用,应立即拒绝请求并返回403状态码,而不是做重定向,因为重定向本身可能被中间人利用。

CSRF防护不能只靠框架默认值

Spring Security从某个版本开始默认开启CSRF防护,但很多开发者为了开发方便直接将其禁用,或者在前后端分离场景下错误地保留了基于Session的CSRF令牌机制。对于纯API服务,如果你的认证完全基于无状态的JWT或OAuth2令牌,且没有使用Cookie传递会话信息,那么禁用CSRF是合理的。但如果你使用了Cookie,哪怕只是用来存放一个临时标识,也必须开启CSRF保护,并且不能使用默认的CookieCsrfTokenRepository,而要结合前端框架的双重提交模式,将令牌放在自定义请求头中,后端从请求头读取并校验。

http
    .csrf()
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
    .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);

自定义过滤器要确保从Cookie中读取令牌后,将其与请求头中的X-XSRF-TOKEN进行比对,任何不匹配都应立即中断请求链。

输入校验必须穿透到每一个DTO字段

SpringBoot的@Validated和@NotBlank等注解很容易被滥用,很多开发者只校验了非空和长度,却忽略了类型安全、格式校验和业务逻辑边界。对于金额字段,不能只校验BigDecimal的精度,还要校验其数值范围是否在业务允许的上下限内。对于手机号和身份证号,必须用正则表达式严格限定格式,并且要考虑到特殊号段和校验位算法。更关键的是,所有从请求体、查询参数、路径变量甚至Header中获取的数据,都必须经过一次自定义的清洗器,去除Unicode控制字符和潜在的SQL元字符,哪怕你用的是JPA参数绑定,也不能完全依赖ORM的防注入机制。

public class OrderCreateDTO {
    
    @NotBlank
    @Pattern(regexp = "^[a-zA-Z0-9\\-]{1,64}$", message = "订单号格式非法")
    private String orderId;

    @NotNull
    @DecimalMin(value = "0.01")
    @DecimalMax(value = "999999.99")
    private BigDecimal amount;

    @NotBlank
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
    private String mobile;
}

在Controller层,除了使用@Valid开启校验,还要在方法体内部对传入的字符串做二次转义,特别是那些最终要拼接进日志或发送到消息队列的数据。

依赖版本锁定与自动化漏洞扫描

SpringBoot的依赖管理虽然方便,但传递依赖中经常潜伏着高危漏洞。你不能等到漏洞公开了才去手动升级。在pom.xml中,必须使用dependencyManagement将所有关键依赖的版本锁定到经过安全审计的特定版本,而不是跟随SpringBoot父项目的默认版本。同时,集成OWASP Dependency-Check Maven插件,在每次构建时自动检查依赖库的已知CVE漏洞,如果发现高危漏洞,构建直接失败。


    org.owasp
    dependency-check-maven
    8.4.0
    
        7
        
            HTML
            JSON
        
    
    
        
            
                check
            
        
    

对于SpringBoot自身,也要定期检查其安全公告,不要等到大版本停止维护了还在使用。升级时,重点测试自动配置的变更是否引入了新的默认开放端口或端点。

序列化与反序列化的白名单控制

如果你的SpringBoot应用接收序列化对象,无论是Java原生序列化还是JSON反序列化,都必须实施严格的白名单机制。Jackson在反序列化时,如果开启了默认类型处理,攻击者可以通过构造特定JSON串来触发远程代码执行。在配置ObjectMapper时,绝对不要使用enableDefaultTyping(),如果业务确实需要多态反序列化,必须指定一个允许的类列表,并且这个列表要尽可能小。

ObjectMapper mapper = new ObjectMapper();
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
    .allowIfBaseType("com.yourcompany.model.")
    .build();
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);

更彻底的做法是,在系统架构层面完全避免接收不可信来源的序列化数据,如果必须接收,就在反序列化之前增加一层签名校验,确保数据来源可信且未被篡改。

日志脱敏与审计追踪

日志中打印用户手机号、身份证、银行卡号甚至密码明文的情况,在真实项目中比比皆是。你需要在日志框架层面实现统一的脱敏转换器。在Logback的PatternLayout中,自定义一个转换符,对匹配到的敏感字段值进行中间字符替换为星号的操作。同时,确保所有涉及敏感操作的业务逻辑,都记录了操作人、操作时间、操作IP和操作内容的不可篡改审计日志,这条日志要异步写入独立的存储介质,并且不能提供删除接口。

public class SensitiveDataConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event) {
        String message = event.getFormattedMessage();
        message = message.replaceAll("(mobile|phone)=\\d{11}", "$1=*");
        message = message.replaceAll("(idCard)=\\d{17}[\\dXx]", "$1=*");
        return message;
    }
}

在logback-spring.xml中注册这个转换器,确保所有通过Logger输出的内容都经过脱敏处理,而不是靠每个开发者在拼接字符串时手动处理。

线程池与资源隔离防止拒绝服务

SpringBoot的@Async注解和内置线程池如果不加限制,一个耗时任务阻塞就可能耗尽所有线程,导致整个服务不可用。你需要为不同业务场景定义独立的线程池,并设置合理的队列容量和拒绝策略。对于核心接口,使用CallerRunsPolicy让调用线程自己执行任务,防止队列无限增长;对于非关键异步任务,使用DiscardOldestPolicy丢弃最旧的任务,保证新任务能及时处理。

@Bean("orderThreadPool")
public Executor orderExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(100);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.setThreadNamePrefix("order-");
    return executor;
}

在Controller层面,还要结合Tomcat或Undertow的连接数限制,在application.yml中设置最大连接数和最大线程数,防止单个恶意客户端通过慢速攻击耗尽连接资源。

server:
  tomcat:
    max-connections: 200
    threads:
      max: 50
    connection-timeout: 5000
跨域配置的精细化管控

CORS配置中,allowedOrigins不要使用通配符*,尤其是当你的接口涉及凭证传递时。必须明确列出允许的域名,并且区分生产环境和测试环境的域名列表。对于allowedMethods,只开放业务需要的GET、POST等,禁止OPTIONS以外的TRACE、CONNECT等危险方法。allowedHeaders也要做白名单限制,拒绝接受随机自定义头。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/")
            .allowedOrigins("https://www.yourdomain.com")
            .allowedMethods("GET", "POST", "PUT")
            .allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
            .exposedHeaders("X-Total-Count")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

对于非浏览器客户端发起的请求,跨域限制并不生效,因此后端接口自身的鉴权不能依赖CORS策略,CORS只是浏览器同源策略的补充,不是安全防护的最终手段。

会话与令牌的安全存储策略

如果使用Spring Session配合Redis存储会话,必须为Redis连接配置密码和SSL,并且设置合理的会话超时时间。对于JWT令牌,绝对不能将敏感信息直接放在Payload中,因为Payload只是Base64编码而非加密。令牌的签名密钥必须使用长度足够的随机字符串,并通过环境变量或密钥管理服务注入,严禁硬编码在配置文件中。同时,在验证令牌时,除了校验签名和过期时间,还要校验令牌的颁发者和受众字段,防止令牌被跨服务滥用。

JwtParser parser = Jwts.parserBuilder()
    .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
    .requireIssuer("your-auth-service")
    .requireAudience("your-resource-service")
    .build();

对于刷新令牌,必须实施轮换策略,每次使用刷新令牌获取新的访问令牌时,旧的刷新令牌立即失效,并颁发一个新的刷新令牌,这样可以有效降低令牌泄露后的风险窗口。

文件上传的深度防御

SpringBoot处理文件上传时,不能仅依赖MultipartFile的getOriginalFilename来获取文件名,因为这个值可以被客户端任意伪造,包含路径遍历字符。你需要用UUID重新生成存储文件名,保留原始扩展名之前,先通过白名单校验扩展名是否在允许范围内。文件内容类型必须通过Magic Number检测,而不是信任客户端传来的Content-Type。文件存储路径必须配置在应用根目录之外,并且为上传目录设置独立的权限,禁止该目录下的任何脚本执行权限。

String originalFilename = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
if (!Arrays.asList("jpg", "png", "pdf").contains(extension.toLowerCase())) {
    throw new IllegalArgumentException("不支持的文件类型");
}
String storedName = UUID.randomUUID().toString() + "." + extension;
Path targetPath = Paths.get(uploadBaseDir, storedName).normalize();
if (!targetPath.startsWith(uploadBaseDir)) {
    throw new SecurityException("路径遍历攻击检测");
}
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);

上传完成后,如果业务允许,最好对图片文件进行重编码处理,去除可能嵌入在EXIF信息中的恶意脚本,对文档文件进行病毒扫描。

配置加密与生产环境隔离

application.yml中存储的数据库密码、API密钥等敏感配置,不能以明文形式随代码一起提交到版本库。使用jasypt-spring-boot对配置值进行加密,主密钥通过环境变量或启动参数注入,绝不出现在配置文件中。同时,严格区分多环境配置,生产环境的配置文件只包含生产环境特有的参数,并通过spring.profiles.active在部署时指定,避免测试环境的宽松配置被误用到生产。

jasypt:
  encryptor:
    password: ${JASYPT_ENCRYPTOR_PASSWORD}
    algorithm: PBEWithMD5AndDES
    iv-generator-classname: org.jasypt.iv.NoIvGenerator

在CI/CD流水线中,增加一个配置检查步骤,扫描所有配置文件,一旦发现包含password、secret、key等关键词且值为明文,立即阻断构建,强制要求改为加密占位符。

自定义安全响应头

Spring Security提供的默认安全响应头往往不够全面。你需要添加一个静态的过滤器,为所有HTTP响应统一添加X-Content-Type-Options、X-Frame-Options、X-XSS-Protection、Referrer-Policy、Permissions-Policy等头信息。特别是Content-Security-Policy头,需要根据你的前端资源来源仔细配置,不能简单地设为default-src 'self',因为这样可能会阻止合法的CDN脚本或内联样式,但也不能为了省事而使用unsafe-inline和unsafe-eval,这会大幅削弱CSP的保护能力。

http
    .headers()
        .contentTypeOptions()
        .and()
        .xssProtection()
        .and()
        .cacheControl()
        .and()
        .frameOptions().deny()
        .and()
        .addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "strict-origin-when-cross-origin"))
        .addHeaderWriter(new StaticHeadersWriter("Permissions-Policy", "camera=(), microphone=(), geolocation=()"));

这些头信息虽然不能阻止所有攻击,但可以大幅提高攻击者的利用门槛,是纵深防御体系中成本最低但收益明显的一环。