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

В каком типе данных хранил бы пароли в приложении

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

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

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

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

# В каком типе данных хранить пароли в приложении

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

Правильный ответ: char[] вместо String

Почему НЕ String?

String в Java immutable (неизменяемый), что создаёт security problem:

// ❌ ЭТО ПЛОХО
public class LoginService {
    public boolean authenticate(String password) {
        String correctPassword = "mySecretPass123";
        return password.equals(correctPassword);
        // Проблемы:
        // 1. String immutable — нельзя перезаписать
        // 2. Пароль остаётся в памяти до GC
        // 3. GC может быть задержана — пароль висит в памяти долго
        // 4. Через memory dump можно вытащить пароль
        // 5. String pool может сохранить копию пароля
    }
}

Визуально:

Heap:
┌─────────────────────────────┐
│ String объект               │
│ ┌─────────────────────────┐ │
│ │ char[] array            │ │
│ │ [m][y][S][e][c][r][e]...│ │
│ └─────────────────────────┘ │
│ value = 0x7FFF0000 (ссылка) │
└─────────────────────────────┘  <- останется в памяти долго!

Почему char[]?

// ✅ ПРАВИЛЬНО
public class LoginService {
    public boolean authenticate(char[] password) {
        char[] correctPassword = {m, y, S, e, c, r, e, t};
        boolean matches = matches(password, correctPassword);
        
        // Очистить массив
        clearArray(password);
        clearArray(correctPassword);
        
        return matches;
    }
    
    private boolean matches(char[] a, char[] b) {
        if (a.length != b.length) return false;
        boolean result = true;
        for (int i = 0; i < a.length; i++) {
            if (a[i] != b[i]) {
                result = false;
            }
        }
        return result;
    }
    
    private void clearArray(char[] array) {
        java.util.Arrays.fill(array, \0);  // Перезаписать нулями
    }
}

Преимущества:

  1. Изменяемый — можно явно перезаписать нулями
  2. Скорое удаление из памяти — нули на месте пароля, нечего красть
  3. Контроль жизненного цикла — точно знаешь, когда пароль очищен
  4. Не попадает в String pool

Spring Security и PasswordEncoder

В реальных приложениях используй Spring Security, который всё делает правильно:

@Service
public class UserService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserRepository userRepository;
    
    public void register(String username, String rawPassword) {
        // ✅ Spring автоматически работает с char[]
        String hashedPassword = passwordEncoder.encode(rawPassword);
        User user = new User(username, hashedPassword);
        userRepository.save(user);
        // rawPassword автоматически очищена Spring
    }
    
    public boolean authenticate(String username, String rawPassword) {
        User user = userRepository.findByUsername(username);
        return passwordEncoder.matches(rawPassword, user.getPassword());
        // matches() работает с char[] внутри
    }
}

Если нужна явная работа с char[]:

@RestController
public class LoginController {
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(HttpServletRequest request) {
        char[] password = request.getParameter("password").toCharArray();
        
        try {
            String username = request.getParameter("username");
            Authentication auth = new UsernamePasswordAuthenticationToken(
                username,
                password
            );
            authenticationManager.authenticate(auth);
            return ResponseEntity.ok("Success");
        } finally {
            // ✅ Очистить пароль
            Arrays.fill(password, \0);
        }
    }
}

Где хранить пароль в БД

В БД НИКОГДА не хранишь открытый пароль. Только хешированный и посоленный:

// CREATE TABLE users (
//     id UUID PRIMARY KEY,
//     username VARCHAR(255) UNIQUE NOT NULL,
//     password_hash VARCHAR(255) NOT NULL,  <- НИКОГДА не открытый пароль
//     salt BYTEA,
//     created_at TIMESTAMPTZ
// );

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue
    private UUID id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column(name = "password_hash", nullable = false)
    private String passwordHash;  // bcrypt, argon2, scrypt
    
    // ✅ Salt интегрирован в bcrypt хеш
}

Правильный алгоритм хеширования

Используй современные, медленные алгоритмы (чтобы brute-force был невозможен):

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // ✅ BCrypt — надёжный выбор
        return new BCryptPasswordEncoder(12);  // strength=12 (медленнее, безопаснее)
    }
    
    // Или ещ�ё лучше — Argon2
    // return new Argon2PasswordEncoder();
}

Сравнение алгоритмов:

MD5/SHA1  → ❌ ЗАПРЕЩЕНО — слишком быстрые, уязвимы к brute-force
SHA256    → ❌ ЗАПРЕЩЕНО — всё ещё слишком быстро
PBKDF2    → ⚠️  Возможно, но старомодно
BCrypt    → ✅ ХОРОШО — медленный, с автоматическим солью
Argon2    → ✅ ОТЛИЧНО — современный, очень медленный
SCrypt    → ✅ ОТЛИЧНО — специально для паролей

Полный пример — правильное приложение

@Service
public class AuthenticationService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public AuthResponse register(RegisterRequest request) {
        // request.password приходит как String из HTTP
        char[] passwordChars = request.getPassword().toCharArray();
        
        try {
            // Хешируем пароль
            String passwordHash = passwordEncoder.encode(
                new String(passwordChars)
            );
            
            User user = new User();
            user.setUsername(request.getUsername());
            user.setPasswordHash(passwordHash);
            user.setCreatedAt(Instant.now(ZoneId.of("UTC")));
            
            userRepository.save(user);
            
            return new AuthResponse("Success");
        } finally {
            // ✅ Очистить пароль
            Arrays.fill(passwordChars, \0);
        }
    }
    
    public AuthResponse login(LoginRequest request) {
        String username = request.getUsername();
        String rawPassword = request.getPassword();
        
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new AuthenticationException("User not found"));
        
        // ✅ Spring Security сама управляет очисткой
        if (passwordEncoder.matches(rawPassword, user.getPasswordHash())) {
            String token = generateJWT(user);
            return new AuthResponse(token);
        }
        
        throw new AuthenticationException("Invalid password");
    }
}

Чеклист Security

  • Используешь char[] для временного хранения паролей
  • Явно очищаешь массив Arrays.fill(password, \0)
  • Используешь PasswordEncoder (BCrypt или Argon2)
  • Пароли в БД только хешированы и посолены
  • Используешь HTTPS (никогда HTTP)
  • Логируешь попытки входа, но не сам пароль
  • Есть rate limiting на login endpoint
  • Используешь безопасные сессии/JWT
  • Пароли не видны в логах, профилировщиках, stack traces

Практический совет

В 99% случаев используй Spring Security — там все security детали уже реализованы правильно. Не пытайся реализовать аутентификацию с нуля.

итак, ответ: char[] для временного хранения, хешированный пароль в БД через PasswordEncoder (BCrypt/Argon2), явная очистка памяти.