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

Как сверяются данные после введения пароля и логина

2.0 Middle🔥 141 комментариев
#Основы Java

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

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

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

Как проверяются пароль и логин в приложении

Процесс аутентификации (логин и проверка пароля) реализуется в Spring Security и состоит из нескольких этапов. Я описываю как это работает в современном приложении.

Общая схема

1. Пользователь вводит логин/пароль → отправляет POST запрос
2. Spring Security перехватывает запрос
3. Загружает пользователя из БД по логину
4. Сравнивает пароли (хешированный из БД с введенным)
5. Если совпадают → создает сессию/токен
6. Если не совпадают → возвращает 401 Unauthorized

1. Контроллер логина

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
    
    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
        try {
            // Пользователь отправляет логин и пароль
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(),
                    request.getPassword()
                )
            );
            
            // Если authenticate() успешна → пароли совпадают
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            // Генерируем JWT токен
            String token = tokenProvider.generateToken(authentication);
            
            return ResponseEntity.ok(new AuthResponse(token));
            
        } catch (BadCredentialsException e) {
            // Пароль неправильный
            return ResponseEntity.status(401)
                .body(new AuthResponse("Invalid username or password"));
        } catch (UsernameNotFoundException e) {
            // Логин не найден
            return ResponseEntity.status(401)
                .body(new AuthResponse("User not found"));
        }
    }
}

@Data
public class LoginRequest {
    private String username;
    private String password;
}

@Data
public class AuthResponse {
    private String token;
}

2. AuthenticationManager — основной класс проверки

AuthenticationManager — это интерфейс который обрабатывает аутентификацию:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    
    // 1. Создаем AuthenticationManager бин
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    // 2. Конфигурируем как загружаются пользователи
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userService.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found: " + username
            ));
    }
    
    // 3. Конфигурируем как проверяются пароли
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3. UserDetailsService — загрузка пользователя

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
    
    private final UserRepository userRepository;
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        
        // Шаг 1: Загружаем пользователя из БД по логину
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> {
                logger.warn("Login attempt with non-existent username: {}", username);
                return new UsernameNotFoundException(
                    "User not found: " + username
                );
            });
        
        logger.debug("User found: {}", username);
        
        // Шаг 2: Преобразуем в UserDetails
        return new CustomUserDetails(
            user.getId(),
            user.getUsername(),
            user.getPassword(), // Хешированный пароль из БД!
            user.getEmail(),
            user.getRoles()
        );
    }
}

4. CustomUserDetails — сущность пользователя

@Data
@Builder
public class CustomUserDetails implements UserDetails {
    
    private Long id;
    private String username;
    private String password; // Хешированный пароль!
    private String email;
    private List<Role> roles;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Преобразуем роли в authorities
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true; // или проверка из БД
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true; // или проверка из БД
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true; // или проверка из БД
    }
    
    @Override
    public boolean isEnabled() {
        return true; // или проверка из БД
    }
}

5. Как работает сравнение пароля

ВАЖНО: Пароль НИКОГДА не хранится в открытом виде!

// При регистрации пользователя — хешируем пароль
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    public void registerUser(RegisterRequest request) {
        // Хешируем пароль перед сохранением
        String hashedPassword = passwordEncoder.encode(request.getPassword());
        
        User user = User.builder()
            .username(request.getUsername())
            .password(hashedPassword) // Сохраняем хеш, не пароль!
            .email(request.getEmail())
            .build();
        
        userRepository.save(user);
    }
}

// При логине — сравниваем пароли
public class PasswordVerification {
    
    private final PasswordEncoder passwordEncoder;
    
    public boolean verifyPassword(String rawPassword, String hashedPassword) {
        // BCrypt может сравнить:
        // rawPassword (то что ввел пользователь) с
        // hashedPassword (то что в БД)
        return passwordEncoder.matches(rawPassword, hashedPassword);
    }
}

6. Этап за этапом: что происходит при login

// ШАГИ аутентификации:

Shag 1: Пользователь отправляет логин/пароль
POST /api/v1/auth/login
{
  "username": "john_doe",
  "password": "mySecurePassword123"
}

Шаг 2: AuthenticationManager получает UsernamePasswordAuthenticationToken
authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(
        "john_doe",              // principal (то что идентифицирует)
        "mySecurePassword123"   // credentials (пароль для проверки)
    )
)

Шаг 3: AuthenticationManager выбирает подходящий AuthenticationProvider
(для UsernamePassword — выбирает DaoAuthenticationProvider)

Шаг 4: DaoAuthenticationProvider выполняет:
    a) Вызывает UserDetailsService.loadUserByUsername("john_doe")
    b) Загружает User из БД
    c) Достает хешированный пароль из БД: "$2a$10$L9..."

Шаг 5: Сравнивает пароли
passwordEncoder.matches(
    "mySecurePassword123",           // То что ввел пользователь
    "$2a$10$L9..."                   // Хеш из БД
)
↓
BCrypt проверяет совпадение (это очень сложный процесс)

Шаг 6: Результат
Если совпадают → возвращает Authentication объект
Если нет → выбрасывает BadCredentialsException

Шаг 7: Если успешно
SecurityContextHolder.setAuthentication(authentication);
Теперь пользователь аутентифицирован и может делать запросы

7. Хеширование пароля: BCrypt

// BCrypt — алгоритм ONE-WAY хеширования
// Нельзя "распаковать" пароль из хеша!

public class BCryptExample {
    
    public static void main(String[] args) {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        
        // Хешируем пароль
        String rawPassword = "myPassword123";
        String hashedPassword = encoder.encode(rawPassword);
        
        // Каждый раз разный хеш! (используется random salt)
        System.out.println(encoder.encode(rawPassword));
        // $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86AGR0Iez3y
        System.out.println(encoder.encode(rawPassword));
        // $2a$10$V5c1cIqM0SeswhvBdZ8f2OJST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
        // Разные хеши, хотя пароль одинаковый!
        
        // Проверяем пароль
        boolean matches1 = encoder.matches(rawPassword, 
            "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86AGR0Iez3y");
        System.out.println(matches1); // true
        
        boolean matches2 = encoder.matches(rawPassword, 
            "$2a$10$V5c1cIqM0SeswhvBdZ8f2OJST9/PgBkqquzi.Ss7KIUgO2t0jWMUW");
        System.out.println(matches2); // true
        
        // Неправильный пароль
        boolean matches3 = encoder.matches("wrongPassword", 
            "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcg7b3XeKeUxWdeS86AGR0Iez3y");
        System.out.println(matches3); // false
    }
}

8. Безопасность: что НЕ НУЖНО делать

// ❌ НИКОГДА ТАК НЕ ДЕЛАЙ!
public boolean verifyPasswordWrong(String inputPassword, String storedPassword) {
    // Простое сравнение строк
    return inputPassword.equals(storedPassword);
    // Проблемы:
    // 1. Пароли видны в памяти
    // 2. Легко подбрать brute-force
    // 3. Нет salt
}

// ❌ Простое хеширование
public String hashWrong(String password) {
    return Integer.toHexString(password.hashCode());
    // Проблемы: легко разломать
}

// ❌ SHA-256 без salt
public String hashWrong2(String password) throws NoSuchAlgorithmException {
    return DigestUtils.sha256Hex(password);
    // Проблемы: радужные таблицы работают
}

// ✅ ПРАВИЛЬНО: BCrypt с автоматическим salt
public String hashCorrect(String password) {
    return new BCryptPasswordEncoder().encode(password);
    // Плюсы:
    // 1. Автоматический random salt
    // 2. Устойчив к брут-форсу (медленный)
    // 3. Адаптивный (можно увеличить сложность со временем)
}

9. JWT токены (альтернатива session)

@Service
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${app.jwtSecret}")
    private String jwtSecret;
    
    @Value("${app.jwtExpirationMs:86400000}") // 24 часов по умолчанию
    private int jwtExpirationMs;
    
    // После успешной аутентификации
    public String generateToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        
        return Jwts.builder()
            .setSubject(userPrincipal.getId().toString())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    // При следующих запросах проверяем токен
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

Итоговая схема

Пользователь                          Сервер
    │                                    │
    │ 1. POST /login                     │
    │ {username, password}               │
    ├───────────────────────────────────>│
    │                                    │ 2. AuthenticationManager
    │                                    │ 3. loadUserByUsername() из БД
    │                                    │ 4. passwordEncoder.matches()
    │                                    │    (BCrypt сравнение)
    │                                    │
    │ 5. 200 OK {JWT token}              │
    │<───────────────────────────────────┤
    │                                    │
    │ 6. Следующий запрос с токеном     │
    │ Header: Authorization: Bearer JWT  │
    ├───────────────────────────────────>│
    │                                    │ 7. Проверяем токен
    │                                    │ 8. Если валидный → обрабатываем
    │ 9. 200 OK {data}                   │
    │<───────────────────────────────────┤

Best Practices

  1. ВСЕГДА используй PasswordEncoder (BCrypt, Argon2)
  2. НИКОГДА не логируй пароли
  3. Используй HTTPS для передачи пароля
  4. HTTPS only cookies для сессий
  5. CSRF protection при использовании cookies
  6. Rate limiting для защиты от брут-форса
  7. Двухфакторная аутентификация для повышенной безопасности

Этот процесс обеспечивает безопасное хранение и проверку пароля.

Как сверяются данные после введения пароля и логина | PrepBro