← Назад к вопросам

Почему не рекомендуется использовать строки для хранения паролей?

2.0 Middle🔥 141 комментариев
#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему пароли нельзя хранить в String'ах? (Security Best Practice)

Это критически важный вопрос для безопасности приложений. Расскажу детально о проблемах и как их решать.

Основная проблема: String'и НЕИЗМЕНЯЕМЫ

// ПРОБЛЕМА 1: Strings неизменяемы (immutable)
public class PasswordExample {
    public void loginUser(String password) {
        // password хранится в памяти как String
        // Это серьёзная проблема
    }
}

// String в памяти:
// "mySecurePassword123" → как 19 символов в памяти
// Даже если переменная выходит из scope:
String password = readPasswordFromInput();
if (isPasswordCorrect(password)) {
    login();
}
// Переменная 'password' удалена, но String остался в памяти!
// Он очистится только при сборке мусора (может быть через часы)

// ПРОБЛЕМА 2: Копии String'а в памяти
String password = "user_password";
String copy = password;           // Та же ссылка (новой копии нет)
String concat = password + "!";   // Новый String в памяти
String substring = password.substring(0, 4);  // Ещё один String в памяти
String lower = password.toLowerCase();  // Ещё один String

// РЕЗУЛЬТАТ: пароль может быть в памяти в 5+ местах!
// Все эти копии — потенциальный источник утечки

Основное решение: char[] вместо String

// ПРАВИЛЬНО: использовать char[]

public class SecurePasswordHandling {
    
    // НИКОГДА не передавай пароль как String
    // ВСЕГДА используй char[]
    
    public void secureLogin(char[] password) {
        try {
            // Работаем с char[]
            boolean isValid = validatePassword(password);
            
            if (isValid) {
                authenticate();
            }
        } finally {
            // ОБЯЗАТЕЛЬНО: очистить массив после использования
            clearPassword(password);
        }
    }
    
    // Очистка пароля из памяти
    private void clearPassword(char[] password) {
        if (password != null) {
            for (int i = 0; i < password.length; i++) {
                password[i] = '\0';  // Заменяем на нули
            }
        }
    }
    
    // Сравнение паролей без создания новых String'ов
    private boolean validatePassword(char[] inputPassword) {
        char[] storedPassword = retrieveHashedPassword();  // хеш, не сам пароль
        
        // Сравнение с защитой от timing attacks
        return MessageDigest.isEqual(
            hashPassword(inputPassword),
            storedPassword
        );
    }
}

Почему char[] безопаснее

// ПРЕИМУЩЕСТВО 1: char[] ИЗМЕНЯЕМЫЕ
char[] password = "myPassword".toCharArray();
password[0] = '\0';  // ✅ Можно изменить
password[1] = '\0';
// Теперь пароль стёрт из памяти

// String password = "myPassword";
// password.charAt(0) = 'x';  // ❌ Ошибка! String неизменяем

// ПРЕИМУЩЕСТВО 2: char[] не дублируются
char[] password = readPassword();  // Один массив в памяти
// Если изменим password, изменяем именно эту ячейку

String password = readPassword();  // String в памяти
password.substring(0, 5);  // Создал новый String
password.concat("");       // Создал ещё один String

// ПРЕИМУЩЕСТВО 3: Явный контроль над памятью
char[] password = readPassword();
try {
    processPassword(password);
} finally {
    Arrays.fill(password, '\0');  // ГАРАНТИРОВАННО очистили
}

// String никогда не очистится явно

Полный пример правильной обработки паролей

@Service
public class AuthenticationService {
    
    private final PasswordEncoder encoder;  // Spring Security BCrypt
    
    // API: char[] вместо String
    public boolean authenticate(String username, char[] password) {
        try {
            User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UnauthorizedException());
            
            // Сравнение с хешированным паролем
            boolean isValid = encoder.matches(
                String.valueOf(password),  // Временный String!
                user.getPasswordHash()      // Хеш (не пароль)
            );
            
            return isValid;
        } finally {
            // ОБЯЗАТЕЛЬНО очистить
            clearPassword(password);
        }
    }
    
    // Регистрация
    public void register(String username, char[] password, char[] confirmPassword) {
        try {
            // Проверка на совпадение (тоже через char[])
            if (!Arrays.equals(password, confirmPassword)) {
                throw new ValidationException("Passwords don't match");
            }
            
            // Хеширование пароля
            String passwordHash = encoder.encode(String.valueOf(password));
            
            // Сохраняем только ХЕШ, не сам пароль
            User user = new User(username, passwordHash);
            userRepository.save(user);
            
        } finally {
            clearPassword(password);
            clearPassword(confirmPassword);
        }
    }
    
    private void clearPassword(char[] password) {
        if (password != null) {
            Arrays.fill(password, '\0');
        }
    }
}

Spring Security + char[]

// Spring Security интегрирует это правильно

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .formLogin(form -> form
                .loginPage("/login")
                // Spring сам работает с char[]
            )
            .authorizeRequests(auth -> auth
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

// Spring Security UsernamePasswordAuthenticationFilter работает:
// 1. Читает пароль из request
// 2. Сохраняет в char[]
// 3. Передаёт в AuthenticationManager
// 4. Автоматически очищает после использования

Дополнительные проблемы String'ов

// ПРОБЛЕМА 1: String Pool
public class StringPoolProblem {
    public static void main(String[] args) {
        String password = "securePassword";
        // Java может сохранить это в String Pool
        // String Pool остаётся в памяти весь процесс
        // Пароль там остаётся ВЕЧНО (никогда не удалится)
        
        // char[] не попадает в String Pool
        char[] password2 = "securePassword".toCharArray();
        Arrays.fill(password2, '\0');
        // Теперь это удалится при сборке мусора
    }
}

// ПРОБЛЕМА 2: Логирование
String password = getUserPassword();
logger.info("User login attempt: " + password);  // ❌ УЖАС!
// Пароль в логах навечно!

char[] password2 = getUserPassword2();
// Сложнее случайно залогировать char[]

// ПРОБЛЕМА 3: Exception Stack Traces
public void processPassword(String password) {
    try {
        // Код
    } catch (Exception e) {
        // Если пароль в переменной, может попасть в stack trace
        throw new RuntimeException(e);
    }
}

// ПРОБЛЕМА 4: Heap Dumps
// Если случится OutOfMemoryError и создастся heap dump
// String'и с паролями будут видны в hex view

Как хранить пароли в БД

// НИКОГДА так:
public class User {
    private String password;  // ❌ Сохраняет открытый пароль
}

// ПРАВИЛЬНО: только хеш
public class User {
    private String passwordHash;  // ✅ Хеш (одностороннее преобразование)
    
    public boolean checkPassword(char[] password) {
        // Использование BCrypt
        return BCrypt.checkpw(
            String.valueOf(password),
            this.passwordHash
        );
    }
}

// Хеширование паролей:
// 1. Используй BCrypt или Argon2 (медленные, специальные для паролей)
// 2. НИКОГДА не используй MD5, SHA-1
// 3. ВСЕГДА используй salt (BCrypt делает это автоматически)

// ❌ НЕПРАВИЛЬНО (MD5)
String hash = DigestUtils.md5Hex(password);
// MD5 очень быстрый, это плохо для паролей (брутфорс)

// ✅ ПРАВИЛЬНО (BCrypt)
String hash = BCrypt.hashpw(
    String.valueOf(password),
    BCrypt.gensalt(12)  // 12 rounds (медленно — хорошо)
);

// ✅ ПРАВИЛЬНО (Argon2 — современный стандарт)
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(
    16,     // salt length
    32,     // hash length
    1,      // parallelism
    60000,  // memory (KB)
    2       // iterations
);
String hash = encoder.encode(String.valueOf(password));

Чеклист безопасного хранения паролей

public class PasswordSecurityChecklist {
    
    // ✓ 1. Принимай пароли как char[], не String
    public void login(char[] password) { }
    
    // ✓ 2. Очищай char[] после использования
    Arrays.fill(password, '\0');
    
    // ✓ 3. Никогда не логируй пароли
    // ✓ 4. Никогда не показывай пароль в исключениях
    // ✓ 5. Храни только ХЕШ, не открытый пароль
    // ✓ 6. Используй BCrypt, Argon2, PBKDF2
    // ✓ 7. Используй уникальный salt (BCrypt делает автоматически)
    // ✓ 8. Используй высокую сложность хеширования
    // ✓ 9. Никогда не возвращай пароль клиенту
    // ✓ 10. HTTPS для передачи паролей
    // ✓ 11. Использование 2FA, MFA
    // ✓ 12. Rate limiting на endpoints логина
    
    public boolean isPasswordSecure(char[] password) {
        return password.length >= 12  // Минимум 12 символов
            && hasUppercase(password)
            && hasLowercase(password)
            && hasDigits(password)
            && hasSpecialChars(password);
    }
}

Выводы

Почему не String'и для паролей:

  1. Неизменяемость — String'и нельзя стереть
  2. Дублирование — копии в памяти
  3. String Pool — хранится вечно
  4. Сборка мусора — паролемогут оставаться часами
  5. Безопасность памяти — уязвимо к heap dumps

Решение:

  • ✅ Используй char[] для работы с паролями
  • ✅ Очищай char[] сразу после использования (Arrays.fill())
  • ✅ Храни только ХЕШ, не сам пароль
  • ✅ Используй BCrypt или Argon2 для хеширования
  • ✅ Используй HTTPS для передачи

Это базовый стандарт безопасности для любого приложения, работающего с паролями.