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

Как шифровал в Spring Security

2.0 Middle🔥 191 комментариев
#Spring Framework

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

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

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

Шифрование паролей в Spring Security: практический подход

Одна из критичных задач в любом приложении — безопасное хранение паролей. Spring Security предоставляет несколько механизмов, и я расскажу о лучших практиках.

Главное правило

НИКОГДА не сохраняйте пароли в открытом виде! Нужно использовать необратимое хеширование (хеширование, а не шифрование).

Решение 1: BCryptPasswordEncoder (РЕКОМЕНДУЕТСЯ)

BCrypt — это специально разработанный алгоритм для паролей, включает соль и адаптивную стоимость:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // По умолчанию strength = 10 (хороший компромисс)
        return new BCryptPasswordEncoder();
    }
}

// Использование при регистрации
@Service
public class UserService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }
    
    public User registerUser(String email, String plainPassword) {
        // Хешируем пароль перед сохранением
        String hashedPassword = passwordEncoder.encode(plainPassword);
        
        User user = new User();
        user.setEmail(email);
        user.setPassword(hashedPassword);  // Сохраняем хеш
        
        return userRepository.save(user);
    }
    
    // Проверка при входе
    public boolean authenticateUser(String email, String plainPassword) {
        User user = userRepository.findByEmail(email).orElseThrow();
        
        // Сравниваем введённый пароль с сохранённым хешем
        return passwordEncoder.matches(plainPassword, user.getPassword());
    }
}

Как работает BCrypt:

  1. Соль (Salt) — случайное значение генерируется для каждого пароля
  2. Хеширование — пароль + соль хешируются несколько раз
  3. Стоимость — параметр strength замедляет хеширование (защита от brute force)
  4. Нестабильный результат — каждый раз при encode() получается разный хеш
  5. Проверка — matches() сравнивает plaintext пароль с сохранённым хешем
// Пример хешей BCrypt
String password = "mySecretPassword123";

// Каждый раз разные хеши:
String hash1 = passwordEncoder.encode(password);
// $2a$10$N9qo8uLOickgxCVjdHxztu3z123123zs8zAz...

String hash2 = passwordEncoder.encode(password);
// $2b$10$R9h21cIWT6g5IKxYe9z5Ye1312312zQ8zH...

// Но matches работает
passwordEncoder.matches(password, hash1);  // true
passwordEncoder.matches(password, hash2);  // true

// Неправильный пароль
passwordEncoder.matches("wrongPassword", hash1);  // false

Настройка уровня стоимости:

@Bean
public PasswordEncoder passwordEncoder() {
    // strength: 4-31 (по умолчанию 10)
    // Выше = медленнее = безопаснее от brute force
    return new BCryptPasswordEncoder(12);  // Используем 12 вместо 10
}

// Benchmark (примерно):
// strength=4:  ~10ms
// strength=10: ~100ms (default)
// strength=12: ~400ms
// strength=15: ~1 сек

Решение 2: Argon2PasswordEncoder (СОВРЕМЕННЫЙ СТАНДАРТ)

Argon2 — это более современный и криптографически стойкий алгоритм:

<!-- Нужна дополнительная зависимость -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>6.0.0</version>
</dependency>
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;

@Configuration
public class SecurityConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // Argon2 параметры
        // salt length = 16 bytes (default)
        // hash length = 32 bytes (default)  
        // parallelism = 1 (default)
        // memory = 4096 KB (default)
        // iterations = 3 (default)
        return new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
    }
}

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

  • Выигрывает криптографические конкурсы
  • Более устойчива к GPU атакам
  • Параметры можно настроить
  • Медленнее BCrypt (защита от brute force)

Решение 3: PBKDF2PasswordEncoder (АЛЬТЕРНАТИВА)

Если нужна стандартизация NIST:

import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    // PBKDF2WithHmacSHA256
    return new Pbkdf2PasswordEncoder(
        "secret",           // secretKey
        "salt",             // salt
        310000,             // iterations
        256                 // hashWidth
    );
}

Решение 4: Делегированный encoder (для миграции)

Если мигрируете со старого алгоритма:

import org.springframework.security.crypto.password.DelegatingPasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    // Поддерживаем несколько алгоритмов
    String defaultEncodingId = "bcrypt";
    
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    
    return new DelegatingPasswordEncoder(defaultEncodingId, encoders);
}

// Это позволяет иметь разные алгоритмы для старых и новых паролей
// Старый пароль: {pbkdf2}$2y$10$R9h...
// Новый пароль: {bcrypt}$2b$10$N9q...

Полный пример Spring Security конфигурации

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    private final UserDetailsService userDetailsService;
    
    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
    
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = 
            http.getSharedObject(AuthenticationManagerBuilder.class);
        
        authenticationManagerBuilder
            .userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
        
        return authenticationManagerBuilder.build();
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(); // Для примера
        
        return http.build();
    }
}

Регистрация и вход

@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
    
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;
    
    @PostMapping("/register")
    public ResponseEntity<UserResponse> register(@RequestBody RegisterRequest request) {
        // Сервис сам зашифрует пароль
        User user = userService.registerUser(request.getEmail(), request.getPassword());
        return ResponseEntity.ok(new UserResponse(user));
    }
    
    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
        try {
            // Spring Security сам проверит пароль через PasswordEncoder
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getEmail(),
                    request.getPassword()
                )
            );
            
            // Генерируем JWT или сессию
            String token = generateJWT(authentication.getPrincipal());
            return ResponseEntity.ok(new LoginResponse(token));
        } catch (BadCredentialsException e) {
            throw new InvalidCredentialsException("Email or password is incorrect");
        }
    }
}

UserDetailsService для Spring Security

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
        
        // Возвращаем UserDetails с хешированным паролем
        return org.springframework.security.core.userdetails.User
            .builder()
            .username(user.getEmail())
            .password(user.getPassword())  // Это хеш, не plain password!
            .authorities(user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList())
            )
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .disabled(false)
            .build();
    }
}

Тестирование

@SpringBootTest
class AuthControllerTest {
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Test
    void passwordEncodingWorks() {
        String plainPassword = "MySecurePassword123!";
        
        // Кодируем
        String encoded = passwordEncoder.encode(plainPassword);
        
        // Проверяем что хеш не равен plain
        assertThat(encoded).isNotEqualTo(plainPassword);
        
        // Но matches находит соответствие
        assertThat(passwordEncoder.matches(plainPassword, encoded)).isTrue();
        
        // Неправильный пароль не подходит
        assertThat(passwordEncoder.matches("WrongPassword", encoded)).isFalse();
    }
    
    @Test
    void bcryptSaltIsRandom() {
        String plainPassword = "SamePassword";
        
        // Кодируем один и тот же пароль дважды
        String encoded1 = passwordEncoder.encode(plainPassword);
        String encoded2 = passwordEncoder.encode(plainPassword);
        
        // Результаты разные (разная соль)
        assertThat(encoded1).isNotEqualTo(encoded2);
        
        // Но оба подходят к plain password
        assertThat(passwordEncoder.matches(plainPassword, encoded1)).isTrue();
        assertThat(passwordEncoder.matches(plainPassword, encoded2)).isTrue();
    }
}

Важные моменты

1. Никогда не сравнивайте хеши напрямую:

// ❌ НЕПРАВИЛЬНО
if (user.getPassword().equals(hashFromDB)) {  // Не работает!
    // ...
}

// ✅ ПРАВИЛЬНО
if (passwordEncoder.matches(plainPassword, user.getPassword())) {
    // ...
}

2. Хешируйте только пароли, не куда-либо сохраняйте plain:

// ❌ НЕПРАВИЛЬНО
public User registerUser(String email, String plainPassword) {
    User user = new User();
    user.setEmail(email);
    user.setPassword(plainPassword);  // БЕЗ хеширования!
    return userRepository.save(user);
}

// ✅ ПРАВИЛЬНО
public User registerUser(String email, String plainPassword) {
    User user = new User();
    user.setEmail(email);
    user.setPassword(passwordEncoder.encode(plainPassword));  // С хешированием!
    return userRepository.save(user);
}

3. Увеличивайте strength для новых приложений:

// Более медленные алгоритмы = защита от brute force
return new BCryptPasswordEncoder(14);  // Более затратный чем 10

4. Когда пользователь меняет пароль:

public void changePassword(User user, String oldPassword, String newPassword) {
    // Проверяем старый пароль
    if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
        throw new InvalidPasswordException("Old password is incorrect");
    }
    
    // Хешируем новый пароль
    user.setPassword(passwordEncoder.encode(newPassword));
    userRepository.save(user);
}

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

АлгоритмСкоростьБезопасностьПоддержка
BCryptсредняявысокая✅ встроено
Argon2медленнаяочень высокая✅ встроено
PBKDF2быстраяхорошая✅ встроено
SCryptмедленнаявысокая✅ встроено

Мой выбор для production

// Для новых проектов
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

// Для высокого уровня безопасности (финансовые приложения)
@Bean
public PasswordEncoder passwordEncoder() {
    return new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
}

// Для миграции со старого алгоритма
@Bean
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder(12));
    encoders.put("legacy", new LegacyEncoder());  // Старый алгоритм
    return new DelegatingPasswordEncoder("bcrypt", encoders);
}

Забезпечение паролей — это критический компонент безопасности приложения. Никогда не пренебрегайте этим.