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

Как сервер понимает, кого можно пропустить

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

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

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

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

Как сервер понимает, кого можно пропустить: аутентификация и авторизация

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

1. Процесс аутентификации

Аутентификация — это определение личности пользователя (кто это?).

Этап 1: Вход (Login)

Пользователь отправляет учетные данные:

// REST API для входа
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
    private final AuthService authService;
    private final JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public LoginResponse login(@RequestBody LoginRequest request) {
        // Проверяем email и пароль
        User user = authService.authenticate(request.getEmail(), request.getPassword());
        
        // Генерируем JWT токен
        String token = tokenProvider.generateToken(user);
        
        return new LoginResponse(token, user.getId(), user.getEmail());
    }
}

// DTO для запроса
public class LoginRequest {
    private String email;
    private String password; // НЕ хранить в логах!
}

// Сервис аутентификации
@Service
public class AuthService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    public User authenticate(String email, String password) {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new AuthException("User not found"));
        
        // Проверяем хэш пароля
        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            throw new AuthException("Invalid password");
        }
        
        return user;
    }
}

Часто для хэширования используют bcrypt:

@Configuration
public class SecurityConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Этап 2: Генерирование токена (JWT)

Вместо отправки пароля каждый раз, сервер генерирует токен — временное удостоверение:

@Component
public class JwtTokenProvider {
    private final String jwtSecret = "my-secret-key-very-long-and-secure";
    private final long jwtExpirationMs = 3600000; // 1 час
    
    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getId().toString()) // Кто это?
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    public String getUserIdFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

Токен выглядит так: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...

Этап 3: Отправка токена в каждом запросе

Клиент отправляет токен в заголовке Authorization:

GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...

2. Проверка токена на сервере

Фильтр аутентификации

Spring использует фильтры для перехвата запросов и проверки токена:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider tokenProvider;
    private final UserRepository userRepository;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        try {
            // 1. Извлекаем токен из заголовка
            String token = extractTokenFromRequest(request);
            
            if (token != null && tokenProvider.validateToken(token)) {
                // 2. Получаем ID пользователя из токена
                String userId = tokenProvider.getUserIdFromToken(token);
                
                // 3. Загружаем пользователя из БД
                User user = userRepository.findById(UUID.fromString(userId))
                    .orElseThrow(() -> new UsernameNotFoundException("User not found"));
                
                // 4. Создаем Authentication объект (скажем Spring, что это автентифицирован пользователь)
                UserDetails userDetails = new UserDetailsImpl(user);
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                
                // 5. Сохраняем в Security Context
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Authentication error", e);
        }
        
        // Передаем управление дальше
        filterChain.doFilter(request, response);
    }
    
    private String extractTokenFromRequest(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

Security Context

Основной объект, где Spring хранит информацию о текущем пользователе:

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @GetMapping("/me")
    public UserDTO getCurrentUser() {
        // Spring автоматически предоставляет текущего пользователя
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new UnauthorizedException("Not authenticated");
        }
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return new UserDTO(userDetails.getUsername());
    }
    
    // Или проще — через аннотацию
    @GetMapping("/me2")
    public UserDTO getCurrentUserSimple(@AuthenticationPrincipal UserDetails userDetails) {
        return new UserDTO(userDetails.getUsername());
    }
}

3. Авторизация — кто может делать что?

Это определение прав доступа (что может делать пользователь?).

Role-based access control (RBAC)

// Объект User с ролями
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

@Entity
@Table(name = "roles")
public class Role {
    @Id
    private Long id;
    
    @Column(unique = true)
    private String name; // "ROLE_ADMIN", "ROLE_USER", "ROLE_MODERATOR"
}

Проверка ролей в контроллере

@RestController
@RequestMapping("/api/v1/admin")
public class AdminController {
    
    // Только ADMIN может делать DELETE
    @DeleteMapping("/users/{userId}")
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(@PathVariable UUID userId) {
        userService.deleteUser(userId);
    }
    
    // Только ADMIN или MODERATOR
    @PutMapping("/users/{userId}")
    @PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
    public UserDTO updateUser(@PathVariable UUID userId, @RequestBody UpdateUserRequest request) {
        return userService.updateUser(userId, request);
    }
    
    // Проверка через код
    @GetMapping("/reports")
    public List<Report> getReports() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        boolean isAdmin = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        
        if (!isAdmin) {
            throw new AccessDeniedException("Only admins can view reports");
        }
        
        return reportService.getAllReports();
    }
}

Permission-based access control (PBAC)

Для более гибких прав:

// Разрешение
@Entity
@Table(name = "permissions")
public class Permission {
    @Id
    private Long id;
    private String name; // "delete_user", "create_post", "edit_comment"
}

// Роль содержит разрешения
@Entity
@Table(name = "roles")
public class Role {
    @ManyToMany
    @JoinTable(
        name = "role_permissions",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "permission_id")
    )
    private Set<Permission> permissions;
}

// Проверка в коде
@Service
public class PostService {
    private final PermissionChecker permissionChecker;
    
    public void deletePost(UUID postId, UUID userId) {
        if (!permissionChecker.hasPermission(userId, "delete_post")) {
            throw new AccessDeniedException("You don't have permission to delete posts");
        }
        
        postRepository.deleteById(postId);
    }
}

4. Spring Security конфигурация

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // CSRF защита для SPA
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT stateless
            .and()
            .authorizeRequests()
                // Публичные endpoints
                .antMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll()
                // Требуют аутентификации
                .antMatchers("/api/v1/users/**").authenticated()
                // Требуют ADMIN роль
                .antMatchers("/api/v1/admin/**").hasRole("ADMIN")
                // Все остальные требуют аутентификацию
                .anyRequest().authenticated()
            .and()
            // Добавляем JWT фильтр
            .addFilterBefore(new JwtAuthenticationFilter(...), UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

5. Различные типы аутентификации

Basic Auth

Authorization: Basic base64(username:password)

Употребляется редко, небезопасно через HTTP.

OAuth 2.0

// Spring может использовать Google, GitHub для входа
@Configuration
public class OAuth2Config {
    // Конфигурация Google OAuth 2.0
}

API Key

// Простой способ для микросервисов
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
        String apiKey = request.getHeader("X-API-Key");
        
        if (apiKey != null && isValidApiKey(apiKey)) {
            // Пропускаем
        } else {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        
        filterChain.doFilter(request, response);
    }
}

6. Безопасные практики

  1. Никогда не отправляйте пароли

    • Используйте хеширование (bcrypt, scrypt)
    • Используйте токены (JWT, OAuth)
  2. HTTPS везде

    • Токен может быть перехвачен по HTTP
  3. CORS защита

    @Configuration
    public class CorsConfig {
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/api/**")
                        .allowedOrigins("https://example.com")
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowedHeaders("*");
                }
            };
        }
    }
    
  4. Rate limiting

    // Защита от brute force
    if (loginAttempts.getOrDefault(email, 0) > 5) {
        throw new TooManyLoginAttemptsException();
    }
    
  5. Логирование без чувствительных данных

    logger.info("User login attempt: email={}, success={}", maskEmail(email), success);
    

Заключение

Основной поток:

  1. Пользователь логинится с email/password
  2. Сервер проверяет пароль и генерирует JWT токен
  3. Клиент отправляет токен в каждом запросе
  4. Сервер проверяет токен через фильтр
  5. Если валиден, запрос разрешен (авторизация проверяет роли)
  6. Если невалиден — 401 Unauthorized

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