Аутентификация и авторизация в Java с помощью JWT Руководство для начинающих

Салимжанов Р.

Введение: зачем нужна безопасность?

Представьте, что вы заходите в банк. Прежде чем получить доступ к своему счёту, вы должны:

  1. Аутентифицироваться — показать паспорт (доказать, что вы это вы).
  2. Авторизоваться — получить доступ только к своим операциям (ваши права).

В веб-приложениях всё аналогично:

  • Аутентификация — проверка логина/пароля.
  • Авторизация — проверка прав доступа к ресурсам.

JWT (JSON Web Token) — это цифровой "пропуск", который выдаётся после успешной аутентификации и используется для авторизации в последующих запросах.

Что такое JWT и как он работает?

Структура JWT:

Header — тип токена и алгоритм шифрования.

{ "alg": "HS512", "typ": "JWT" }

Payload — данные пользователя (например, имя и роли).

{ "username": "user123", "roles": ["ROLE_USER"], "exp": 1630000000 }

Signature — подпись, гарантирующая подлинность токена.

Жизненный цикл JWT:

  1. Пользователь вводит логин/пароль.
  2. Сервер проверяет данные и генерирует JWT.
  3. Клиент сохраняет токен (например, в LocalStorage).
  4. При каждом запросе токен передаётся в заголовке Authorization.
  5. Сервер проверяет подпись и права доступа.

Реализация JWT в Spring Boot

Рассмотрим пример приложения с:

  • Регистрацией/входом
  • Защищённым эндпоинтом
  • Ролевой моделью

Структура проекта (пакеты и файлы)

src/main/java └── com.example.app1test ├── controller │ └── AuthController.java # Обработка /login и /register ├── model │ ├── User.java # Модель пользователя │ └── Role.java # Модель роли ├── repository │ ├── UserRepository.java # Работа с пользователями в БД │ └── RoleRepository.java # Работа с ролями в БД ├── dto │ ├── LoginRequest.java # DTO для запроса входа │ ├── RegisterRequest.java # DTO для запроса регистрации │ └── JwtResponse.java # DTO для ответа с токеном ├── jwt │ ├── JwtUtils.java # Генерация/проверка токенов │ └── JwtAuthenticationFilter # Фильтр для проверки токенов └── security ├── SecurityConfig.java # Конфигурация Spring Security └── UserDetailsServiceImpl.java # Загрузка пользователя из БД

Подробный разбор ключевых компонентов

1. AuthController.java (папка controller)

Назначение: Обрабатывает HTTP-запросы для регистрации и входа.

@RestController @RequestMapping("/api") @RequiredArgsConstructor // Lombok: автоматически внедряет зависимости public class AuthController { private final AuthenticationManager authenticationManager; private final JwtUtils jwtUtils; private final UserRepository userRepository; private final RoleRepository roleRepository; private final PasswordEncoder passwordEncoder; @PostMapping("/auth/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { // 1. Создаём объект аутентификации Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword())); // 2. Сохраняем аутентификацию в контексте SecurityContextHolder.getContext().setAuthentication(authentication); // 3. Генерируем токен String jwt = jwtUtils.generateToken((UserDetails) authentication.getPrincipal()); // Возвращаем токен return ResponseEntity.ok(new JwtResponse(jwt)); } @PostMapping("/auth/register") public ResponseEntity<?> register(@RequestBody RegisterRequest request) { // Проверка на существование пользователя if (userRepository.existsByUsername(request.getUsername())) { return ResponseEntity.badRequest().body("Error: Username is already taken!"); } // Создаём нового пользователя User user = new User(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); // Шифруем пароль! // Назначаем роль (обычно "ROLE_USER" создаётся при старте приложения) Role userRole = roleRepository.findByName("ROLE_USER") .orElseThrow(() -> new RuntimeException("Error: Role USER not found.")); user.setRoles(Collections.singleton(userRole)); userRepository.save(user); return ResponseEntity.ok("User registered successfully!"); } @GetMapping("/protected") public String protectedEndpoint() { return "This is protected data!"; } }

2. User.java (папка model)

Назначение: Сущность пользователя для хранения в БД.

@Entity @Data // Lombok - автоматически создает геттеры, сеттеры и т.д. @Table(name = "test_users") // Указываем имя таблицы в БД public class User { @Id // Указывает что это первичный ключ @GeneratedValue(strategy = GenerationType.IDENTITY) // Автоинкремент private Long id; @Column(unique = true, nullable = false, length = 20) // Ограничения для поля private String username; @Column(nullable = false, length = 100) // Пароль будет храниться в зашифрованном виде private String password; @ManyToMany(fetch = FetchType.EAGER) // Связь многие-ко-многим, загрузка ролей сразу @JoinTable( name = "test_users_roles", // Имя связующей таблицы joinColumns = @JoinColumn(name = "user_id"), // Столбец для текущей сущности inverseJoinColumns = @JoinColumn(name = "role_id")) // Столбец для связанной сущности private Collection<Role> roles; // Список ролей пользователя }

3. Role.java (папка model)

Назначение: Сущность роли для RBAC (Role-Based Access Control).

@Entity @Data @Table(name = "test_roles") // Указываем имя таблицы public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false, length = 20) private String name; // Название роли (например: ROLE_USER, ROLE_ADMIN) }

4. UserRepository.java (папка repository)

Назначение: Интерфейс для работы с таблицей пользователей в БД.

public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); // Поиск пользователя по логину boolean existsByUsername(String username); }

5. RoleRepository.java (папка repository)

Назначение: Интерфейс для работы с ролями.

public interface RoleRepository extends JpaRepository<Role, Long> { Optional<Role> findByName(String name); }

6. LoginRequest.java (папка dto)

Назначение: Объект для передачи данных входа.

@Data // Lombok - автоматические геттеры/сеттеры public class LoginRequest { private String username; // Логин пользователя private String password; // Пароль в открытом виде (передается при аутентификации) }

7. RegisterRequest.java (папка dto)

Назначение: Объект для передачи данных регистрации.

@Data public class RegisterRequest { private String username; private String password; // Пароль будет зашифрован перед сохранением }

8. JwtResponse.java (папка dto)

Назначение: Объект для ответа с JWT-токеном.

@Data public class JwtResponse { private String token; // JWT токен для последующих запросов public JwtResponse(String token) { this.token = token; } }

9. JwtUtils.java (папка jwt)

Назначение: Генерация и проверка JWT.

@Component public class JwtUtils { @Value("${jwt.secret}") private String secret; // Секретный ключ из application.properties @Value("${jwt.expiration}") private long expiration; // Время жизни токена // Генерация JWT токена public String generateToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) // Устанавливаем имя пользователя .setIssuedAt(new Date()) // Время создания .setExpiration(new Date(System.currentTimeMillis() + expiration)) // Время истечения .signWith(SignatureAlgorithm.HS512, secret) // Алгоритм подписи .compact();// Собираем токен } // Валидация токена public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (SignatureException e) { System.err.println("Неверная подпись токена: " + e.getMessage()); } catch (MalformedJwtException e) { System.err.println("Некорректный токен: " + e.getMessage()); } catch (ExpiredJwtException e) { System.err.println("Токен просрочен: " + e.getMessage()); } catch (UnsupportedJwtException e) { System.err.println("Неподдерживаемый токен: " + e.getMessage()); } catch (IllegalArgumentException e) { System.err.println("Пустой токен: " + e.getMessage()); } return false; } // Получение имени пользователя из токена public String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody() .getSubject(); } }

10. JwtAuthenticationFilter.java (папка jwt)

Назначение: перехватывает запросы и проверяет JWT.

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; private final UserDetailsServiceImpl userDetailsService; public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService) { this.jwtUtils = jwtUtils; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1. Получаем заголовок Authorization final String authHeader = request.getHeader("Authorization"); // 2. Проверяем наличие и формат заголовка if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // 3. Извлекаем токен (после "Bearer ") final String jwt = authHeader.substring(7); // 4. Проверяем токен if (jwtUtils.validateToken(jwt)) { // 5. Получаем имя пользователя String username = jwtUtils.getUsernameFromToken(jwt); // 6. Загружаем пользователя из БД UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 7. Создаём объект аутентификации UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); // 8. Сохраняем в SecurityContext SecurityContextHolder.getContext().setAuthentication(authentication); } // 9. Передаём запрос дальше по цепочке фильтров filterChain.doFilter(request, response); } }

11. SecurityConfig.java (папка security)

Назначение: настраивает Spring Security.

@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) { this.jwtAuthFilter = jwtAuthFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() // Разрешаем доступ к аутентификации .requestMatchers("/api/protected").authenticated() // Защищаем конкретный эндпоинт .anyRequest().authenticated() // Все остальные запросы требуют аутентификации ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)// Не используем сессии ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12);// Сила шифрования } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

12. UserDetailsServiceImpl.java (папка security)

Назначение: преобразует нашего User в объект Spring Security.

@Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; public UserDetailsServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(user.getRoles().stream() .map(role -> role.getName()) .toArray(String[]::new)) .build(); } }

13. application.properties

Естественно, это пример, не следует в файле хранить jwt.secret , пароли и явки.

# БД spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=postgres spring.datasource.password=secret # JWT jwt.secret=mySuperSecretKeyWithAtLeast512BitsLength_ThisIsJustExampleKeyChangeItInProduction! jwt.expiration=86400000 # 24 часа

Взаимодействие компонентов: Полный цикл

1. Пользователь отправляет POST /api/register

Запрос → AuthController.register()

→ UserRepository.save()

→ PasswordEncoder.encode()

2. Пользователь входит через POST /api/login

Запрос → AuthController.login()

→ AuthenticationManager.authenticate()

→ JwtUtils.generateToken()

3. Пользователь обращается к защищённому эндпоинту /protected

Запрос → JwtAuthenticationFilter:

1. Проверяет заголовок Authorization

2. Извлекает токен

3. JwtUtils.validateToken()

4. UserDetailsServiceImpl.loadUserByUsername()

5. Устанавливает аутентификацию в SecurityContext

→ SecurityConfig разрешает доступ

→ Метод контроллера возвращает данные

Как всё соединяется воедино?

  1. Spring Security управляет аутентификацией через AuthenticationManager.
  2. JwtAuthenticationFilter перехватывает каждый запрос и проверяет JWT.
  3. UserDetailsServiceImpl связывает наших пользователей из БД с Spring Security.
  4. SecurityConfig определяет, какие URL защищены, а какие доступны всем.
  5. JwtUtils – «мозг» работы с токенами: создание и проверка.
  6. AuthController – точка входа для регистрации и входа.

Пример работы для запросов

Регистрация:

curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"testuser\", \"password\":\"testpass\"}" http://localhost:8080/api/auth/register
Аутентификация и авторизация в Java с помощью JWT Руководство для начинающих

Если повторно регистрироваться:

Аутентификация и авторизация в Java с помощью JWT Руководство для начинающих

Логин (получение токена):

curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"testuser\", \"password\":\"testpass\"}" http://localhost:8080/api/auth/login
Аутентификация и авторизация в Java с помощью JWT Руководство для начинающих

Доступ к защищенному эндпоинту (с токеном):

curl -X GET -H "Authorization: Bearer <ваш_токен>" http://localhost:8080/api/protected
Аутентификация и авторизация в Java с помощью JWT Руководство для начинающих

Типичные ошибки новичков

  1. Не использовать HTTPS Токены перехватываются в открытых сетях.
  2. Хранение секретного ключа в коде Используйте переменные окружения или Vault.
  3. Долгий срок жизни токена Рекомендуется 15-30 минут. Для продления — используйте refresh-токены.

Заключение

Мы реализовали БАЗОВУЮ систему безопасности с JWT! Помните: безопасность — это процесс, а не разовое действие. Всегда следите за обновлениями зависимостей и используйте современные практики.

1
Начать дискуссию