Как сервер понимает, кого можно пропустить
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сервер понимает, кого можно пропустить: аутентификация и авторизация
Это ключевой вопрос о безопасности. Когда пользователь делает запрос к серверу, сервер должен определить его личность и права доступа. Рассмотрю все основные механизмы.
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. Безопасные практики
-
Никогда не отправляйте пароли
- Используйте хеширование (bcrypt, scrypt)
- Используйте токены (JWT, OAuth)
-
HTTPS везде
- Токен может быть перехвачен по HTTP
-
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("*"); } }; } } -
Rate limiting
// Защита от brute force if (loginAttempts.getOrDefault(email, 0) > 5) { throw new TooManyLoginAttemptsException(); } -
Логирование без чувствительных данных
logger.info("User login attempt: email={}, success={}", maskEmail(email), success);
Заключение
Основной поток:
- Пользователь логинится с email/password
- Сервер проверяет пароль и генерирует JWT токен
- Клиент отправляет токен в каждом запросе
- Сервер проверяет токен через фильтр
- Если валиден, запрос разрешен (авторизация проверяет роли)
- Если невалиден — 401 Unauthorized
Это позволяет серверу отличать авторизованных пользователей от злоумышленников.