本文目录导读:

我将通过一个完整案例展示Java常见漏洞的防护方法,涵盖OWASP Top 10中的关键风险。
项目基础设置
Maven依赖配置
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库加密 -->
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- XSS过滤 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>
</dependencies>
SQL注入防护案例
不安全示例
// 不安全的SQL拼接
@GetMapping("/user/search")
public List<User> searchUser(String username) {
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
return jdbcTemplate.query(sql, userRowMapper);
}
安全防护实现
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 安全方式1:使用参数化查询
public List<User> searchUserSafely(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.query(sql,
new Object[]{username},
(rs, rowNum) -> mapUser(rs));
}
// 安全方式2:使用JPA
@Query("SELECT u FROM User u WHERE u.username = :username")
List<User> findByUsername(@Param("username") String username);
// 安全方式3:使用NamedParameterJdbcTemplate
public List<User> searchUserWithNamedParams(String username) {
String sql = "SELECT * FROM users WHERE username = :username";
Map<String, Object> params = new HashMap<>();
params.put("username", username);
return namedParameterJdbcTemplate.query(sql, params, (rs, rowNum) -> mapUser(rs));
}
private User mapUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
return user;
}
}
XSS攻击防护案例
全局XSS过滤器
@Component
public class XSSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
XSSRequestWrapper xssRequestWrapper = new XSSRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequestWrapper, response);
}
}
// XSS请求包装器
public class XSSRequestWrapper extends HttpServletRequestWrapper {
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return sanitizeInput(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
String[] sanitized = new String[values.length];
for (int i = 0; i < values.length; i++) {
sanitized[i] = sanitizeInput(values[i]);
}
return sanitized;
}
// 使用Jsoup进行输入清洗
private String sanitizeInput(String input) {
if (input == null) {
return null;
}
// Jsoup清洗HTML
return Jsoup.clean(input, Safelist.basic());
}
}
CSRF防护配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.headers()
.frameOptions().deny()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
// 其他安全配置
http
.sessionManagement()
.sessionFixation().newSession()
.invalidSessionUrl("/login");
}
}
// 前端配合CSRF Token
@RestController
public class CSRFController {
@GetMapping("/csrf-token")
public CsrfToken getCsrfToken(CsrfToken csrfToken) {
return csrfToken;
}
}
文件上传安全防护
@Component
public class SafeFileUploadService {
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
private static final Set<String> ALLOWED_EXTENSIONS =
Set.of("jpg", "png", "gif", "pdf", "doc", "docx");
public String uploadFile(MultipartFile file, String uploadDir) {
// 1. 验证文件大小
if (file.getSize() > MAX_FILE_SIZE) {
throw new FileUploadException("文件大小超过限制");
}
// 2. 验证文件扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isEmpty()) {
throw new FileUploadException("文件名为空");
}
String extension = getExtension(originalFilename).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(extension)) {
throw new FileUploadException("不支持的文件类型: " + extension);
}
// 3. 验证MIME类型
String mimeType = file.getContentType();
if (!isValidMimeType(mimeType, extension)) {
throw new FileUploadException("MIME类型不匹配");
}
// 4. 生成安全的文件名(使用UUID)
String safeFileName = UUID.randomUUID().toString() + "." + extension;
// 5. 验证路径安全
Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
File uploadDirectory = uploadPath.toFile();
if (!uploadDirectory.exists()) {
uploadDirectory.mkdirs();
}
// 6. 保存文件
try {
InputStream inputStream = file.getInputStream();
Path targetPath = uploadPath.resolve(safeFileName);
// 确保目标路径在允许的目录内
if (!targetPath.normalize().startsWith(uploadPath)) {
throw new FileUploadException("非法路径");
}
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
return safeFileName;
} catch (IOException e) {
throw new FileUploadException("文件保存失败", e);
}
}
private String getExtension(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex >= 0) {
return filename.substring(lastDotIndex + 1);
}
return "";
}
private boolean isValidMimeType(String mimeType, String extension) {
if (mimeType == null) {
return false;
}
Map<String, String> validMimeTypes = Map.of(
"jpg", "image/jpeg",
"jpeg", "image/jpeg",
"png", "image/png",
"gif", "image/gif",
"pdf", "application/pdf",
"doc", "application/msword",
"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
String expectedMimeType = validMimeTypes.get(extension);
return mimeType.equals(expectedMimeType);
}
}
认证与会话安全
@Service
public class SecureAuthService {
private final PasswordEncoder passwordEncoder;
@Autowired
public SecureAuthService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
// 安全密码存储
public User registerUser(String username, String password) {
// 密码强度校验
if (!isStrongPassword(password)) {
throw new WeakPasswordException("密码强度不足");
}
User user = new User();
user.setUsername(username);
// 使用BCrypt加密(自动添加salt)
user.setPassword(passwordEncoder.encode(password));
return userRepository.save(user);
}
// 登录限流
public LoginResult login(String username, String password, String ipAddress) {
// 简单速率限制
if (!rateLimiter.tryAcquire(ipAddress)) {
throw new TooManyRequestsException("登录尝试过于频繁");
}
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent() && passwordEncoder.matches(password, user.get().getPassword())) {
return LoginResult.success();
}
return LoginResult.failed("用户名或密码错误");
}
private boolean isStrongPassword(String password) {
// 至少8位,包含大写、小写、数字和特殊字符
return password.length() >= 8 &&
password.matches(".*[A-Z].*") &&
password.matches(".*[a-z].*") &&
password.matches(".*\\d.*") &&
password.matches(".*[!@#$%^&*()].*");
}
}
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt自带随机salt,是推荐的做法
return new BCryptPasswordEncoder(12); // 12次哈希
}
}
输入验证与请求防护
// DTO验证
public class UserInputValidator {
public static class UserDTO {
@NotNull(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
@Pattern(regexp = "^\\+?[0-9. ()-]{10,25}$", message = "电话号码格式不正确")
private String phone;
// getters and setters
}
}
// 控制器使用
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserDTO userDTO) {
// 业务逻辑
return ResponseEntity.ok("用户创建成功");
}
}
// JSON处理时的额外校验
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
// 防止JSON类型混淆攻击
builder.deserializerByType(String.class,
new JsonDeserializer<String>() {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
String value = p.getValueAsString();
if (value != null && value.length() > 1000) {
throw new IOException("输入字符串过长");
}
return value;
}
});
};
}
}
安全API最佳实践
@RestController
@RequestMapping("/secure")
public class SecureAPIController {
// 1. 统一错误处理
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// 日志记录详细错误信息
log.error("Internal error", e);
// 只返回通用错误信息给客户端
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("服务器内部错误", "请联系管理员"));
}
// 2. 敏感信息过滤
@Jacksonized
@Data
public class UserDTO {
private String username;
@JsonIgnore
private String password; // JSON响应中不显示
private String email;
}
// 3. 安全的CORS配置
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://trusted-domain.com")
.allowedMethods("GET", "POST")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(3600);
}
};
}
// 4. 安全的文件下载
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
// 验证文件名,防止路径遍历
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
return ResponseEntity.badRequest().build();
}
Path filePath = Paths.get("uploads").resolve(fileName).normalize();
Resource resource = new FileSystemResource(filePath);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" +
safeFileName(fileName) + "\"")
.body(resource);
}
private String safeFileName(String originalFileName) {
// 清理文件名中的特殊字符
return originalFileName.replaceAll("[^a-zA-Z0-9.-]", "_");
}
}
安全日志记录与监控
package com.example.security;
@Aspect
@Component
public class SecurityAuditAspect {
private static final Logger securityLog =
LoggerFactory.getLogger("SECURITY_AUDIT");
@Around("@annotation(AuditTrail)")
public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法参数
Object[] args = joinPoint.getArgs();
// 脱敏处理
Object[] sanitizedArgs = sanitizeArgs(args);
// 记录请求
securityLog.info("Security access: method={}, args={}",
joinPoint.getSignature(), sanitizedArgs);
try {
Object result = joinPoint.proceed();
// 记录成功响应
securityLog.info("Operation successful: {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
// 记录失败情况
securityLog.error("Security violation: {}",
joinPoint.getSignature(), e);
throw e;
}
}
private Object[] sanitizeArgs(Object[] args) {
// 对敏感数据进行脱敏
return Arrays.stream(args)
.map(arg -> {
if (arg instanceof String &&
arg.toString().contains("password")) {
return "***";
}
return arg;
})
.toArray();
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditTrail {
String action() default "";
}
安全配置检查清单
# application.properties 安全配置
# 严格会话管理
server.servlet.session.timeout=30m
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=lax
# 数据源加密
jasypt.encryptor.password=${ENCRYPTION_KEY}
jasypt.encryptor.algorithm=PBEWithMD5AndDES
# 安全headers
security.headers.cache-control=true
security.headers.x-frame-options=DENY
security.headers.x-content-type-options=nosniff
security.headers.strict-transport-security=max-age=31536000
# 输入验证
spring.mvc.favicon.enabled=false
这个完整案例展示了Java Web应用中的关键安全防护措施,实际项目中需要根据具体需求选择合适的防护方案,并定期进行安全测试和代码审计。