# Spring Security 6: Pełne uwierzytelnianie JWT > Praktyczny przewodnik wdrażania uwierzytelniania JWT w Spring Security 6: konfiguracja, generowanie tokenów, walidacja i najlepsze praktyki bezpieczeństwa. - Published: 2026-02-19 - Updated: 2026-04-28 - Author: SharpSkill - Tags: spring security, jwt, java, spring boot, authentication - Reading time: 15 min --- Uwierzytelnianie z użyciem JWT (JSON Web Token) stało się standardem zabezpieczania nowoczesnych API REST. Spring Security 6 wprowadza funkcyjne podejście do konfiguracji, które upraszcza wdrożenie i wzmacnia bezpieczeństwo. Ten przewodnik omawia cały proces — od konfiguracji wstępnej po ochronę endpointów. > **Wymagania wstępne** > > Ten tutorial korzysta ze Spring Boot 3.2+ i Spring Security 6.2+. Założenia pozostają prawdziwe w nowszych wersjach przy niewielkich modyfikacjach składni. ## Architektura JWT w Spring Security Uwierzytelnianie JWT opiera się na zasadzie stateless: serwer nie przechowuje żadnej sesji. Każde żądanie zawiera podpisany token potwierdzający tożsamość użytkownika. Taka architektura umożliwia skalowanie horyzontalne bez współdzielenia sesji między instancjami. Proces uwierzytelniania JWT składa się z kilku etapów. Użytkownik uwierzytelnia się danymi logowania, otrzymuje podpisany token JWT, a następnie dołącza go do każdego kolejnego żądania. Serwer weryfikuje podpis i wyciąga z tokenu informacje o użytkowniku. ```java // JwtAuthenticationFlow.java // Conceptual representation of the authentication flow public class JwtAuthenticationFlow { // 1. Initial authentication: POST /api/auth/login // → Verify credentials against database // → Generate signed JWT token // → Return token to client // 2. Authenticated requests: GET /api/protected // → Header: Authorization: Bearer // → Extract and validate token // → Create SecurityContext // → Access protected resource } ``` Takie podejście likwiduje problemy ze sticky session i upraszcza wdrażanie w środowiskach rozproszonych. ## Konfiguracja zależności Maven Projekt wymaga zależności Spring Security oraz biblioteki JWT. JJWT (Java JWT) oferuje płynne i dobrze utrzymywane API do operacji na tokenach. ```xml org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web io.jsonwebtoken jjwt-api 0.12.5 io.jsonwebtoken jjwt-impl 0.12.5 runtime io.jsonwebtoken jjwt-jackson 0.12.5 runtime org.springframework.boot spring-boot-starter-data-jpa ``` Podział na trzy moduły JJWT (api, impl, jackson) realizuje zasadę enkapsulacji: tylko API jest widoczne na etapie kompilacji, a implementacja pozostaje szczegółem czasu wykonania. ## Serwis generowania i walidacji JWT Serwis JWT skupia w jednym miejscu wszystkie operacje na tokenach: generowanie, ekstrakcję claimów i walidację. Bezpieczny klucz tajny podpisuje każdy token i gwarantuje jego integralność. ```java // JwtService.java @Service public class JwtService { // Secret key injected from application.yml @Value("${app.jwt.secret}") private String secretKey; // Token validity duration (24 hours by default) @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Generates a JWT token for an authenticated user public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails); } // Generates a token with custom claims public String generateToken(Map extraClaims, UserDetails userDetails) { return Jwts.builder() .claims(extraClaims) // Additional claims (roles, permissions) .subject(userDetails.getUsername()) // Principal identifier .issuedAt(new Date()) // Creation date .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) .signWith(getSigningKey(), Jwts.SIG.HS256) // HMAC-SHA256 signature .compact(); } // Extracts the username (subject) from the token public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } // Extracts a specific claim via an extraction function public T extractClaim(String token, Function claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } // Validates the token: correct signature and not expired public boolean isTokenValid(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } // Checks if the token has expired private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } // Extracts the expiration date private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } // Parses the token and extracts all claims private Claims extractAllClaims(String token) { return Jwts.parser() .verifyWith(getSigningKey()) // Verifies the signature .build() .parseSignedClaims(token) // Parses the signed token .getPayload(); // Returns the claims } // Generates the signing key from the Base64-encoded secret private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } } ``` Klucz tajny musi być wystarczająco długi (minimum 256 bitów dla HS256) i bezpiecznie przechowywany — nigdy w kodzie źródłowym. > **Bezpieczeństwo klucza tajnego** > > Do klucza JWT należy używać zmiennej środowiskowej lub menedżera sekretów. Wyciek klucza pozwala fałszować ważne tokeny dla dowolnego użytkownika. ## Konfiguracja encji User Encja użytkownika implementuje interfejs `UserDetails` ze Spring Security, dzięki czemu integruje się bezpośrednio z systemem uwierzytelniania. ```java // User.java @Entity @Table(name = "users") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String password; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; // Role stored as enum for type safety @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; // UserDetails implementation: returns user authorities @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } // Username corresponds to email in this implementation @Override public String getUsername() { return email; } // Account always active (adapt as needed) @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } // Getters and setters omitted for brevity } ``` ```java // Role.java public enum Role { USER, // Standard user ADMIN // Administrator with extended privileges } ``` Enum `Role` zawęża zbiór możliwych wartości i upraszcza kontrolę autoryzacji w wyrażeniach SpEL. ## Filtr uwierzytelniania JWT Filtr JWT przechwytuje każde żądanie, aby wydobyć i zwalidować token. Wpina się do łańcucha filtrów Spring Security przed standardowym filtrem uwierzytelniania. ```java // JwtAuthenticationFilter.java @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // Retrieve the Authorization header final String authHeader = request.getHeader("Authorization"); // Check for Bearer prefix if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // Extract the token (without the "Bearer " prefix) final String jwt = authHeader.substring(7); try { // Extract username from token final String userEmail = jwtService.extractUsername(jwt); // Check that user is not already authenticated if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { // Load user details from database UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); // Validate token (signature + expiration + user match) if (jwtService.isTokenValid(jwt, userDetails)) { // Create authentication object UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); // Add request details authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); // Set authentication in security context SecurityContextHolder.getContext().setAuthentication(authToken); } } } catch (ExpiredJwtException e) { // Token expired: user must re-authenticate response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Token expired"); return; } catch (JwtException e) { // Invalid token: incorrect signature or malformed format response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid token"); return; } // Continue the filter chain filterChain.doFilter(request, response); } } ``` `OncePerRequestFilter` gwarantuje, że filtr wykona się tylko raz na żądanie, nawet w przypadku forward lub include. ## Konfiguracja Spring Security 6 Spring Security 6 stosuje funkcyjne podejście z wyrażeniami lambda do konfiguracji łańcucha bezpieczeństwa. Konfiguracja definiuje reguły dostępu i włącza filtr JWT. ```java // SecurityConfig.java @Configuration @EnableWebSecurity @EnableMethodSecurity // Enables @PreAuthorize and @PostAuthorize @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // Disable CSRF since API is stateless (no session cookies) .csrf(csrf -> csrf.disable()) // Configure authorization rules .authorizeHttpRequests(auth -> auth // Public endpoints: authentication and registration .requestMatchers("/api/auth/**").permitAll() // API documentation accessible without authentication .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Admin endpoints reserved for administrators .requestMatchers("/api/admin/**").hasRole("ADMIN") // All other requests require authentication .anyRequest().authenticated() ) // Stateless mode: no server-side HTTP session .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // Custom authentication provider .authenticationProvider(authenticationProvider) // Insert JWT filter before standard authentication filter .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } } ``` ```java // ApplicationConfig.java @Configuration @RequiredArgsConstructor public class ApplicationConfig { private final UserRepository userRepository; // User loading service for Spring Security @Bean public UserDetailsService userDetailsService() { return username -> userRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } // Authentication provider with password encoder @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } // Authentication manager exposed as bean @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } // BCrypt encoder for secure password hashing @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` Wyłączenie CSRF jest bezpieczne dla bezstanowego API, ponieważ uwierzytelnianie opiera się na jawnym nagłówku, a nie na ciasteczku wysyłanym automatycznie przez przeglądarkę. ## Kontroler uwierzytelniania Kontroler udostępnia endpointy rejestracji i logowania. Te endpointy są publiczne i zwracają token JWT po pomyślnym uwierzytelnieniu. ```java // AuthenticationController.java @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authService; // POST /api/auth/register - Register a new user @PostMapping("/register") public ResponseEntity register( @Valid @RequestBody RegisterRequest request ) { return ResponseEntity.ok(authService.register(request)); } // POST /api/auth/login - Login existing user @PostMapping("/login") public ResponseEntity login( @Valid @RequestBody AuthenticationRequest request ) { return ResponseEntity.ok(authService.authenticate(request)); } // POST /api/auth/refresh - Token refresh (optional) @PostMapping("/refresh") public ResponseEntity refresh( @RequestHeader("Authorization") String authHeader ) { return ResponseEntity.ok(authService.refreshToken(authHeader)); } } ``` ```java // AuthenticationRequest.java public record AuthenticationRequest( @NotBlank(message = "Email required") @Email(message = "Invalid email format") String email, @NotBlank(message = "Password required") String password ) {} ``` ```java // RegisterRequest.java public record RegisterRequest( @NotBlank(message = "First name required") String firstName, @NotBlank(message = "Last name required") String lastName, @NotBlank(message = "Email required") @Email(message = "Invalid email format") String email, @NotBlank(message = "Password required") @Size(min = 8, message = "Password must contain at least 8 characters") String password ) {} ``` ```java // AuthenticationResponse.java public record AuthenticationResponse( String token, String type, long expiresIn ) { public AuthenticationResponse(String token, long expiresIn) { this(token, "Bearer", expiresIn); } } ``` Rekordy w Javie upraszczają definicje DTO i zapewniają niezmienność. ## Serwis uwierzytelniania Serwis orkiestruje logikę rejestracji i uwierzytelniania, delegując generowanie tokenów do `JwtService`. ```java // AuthenticationService.java @Service @RequiredArgsConstructor public class AuthenticationService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Register a new user @Transactional public AuthenticationResponse register(RegisterRequest request) { // Check that email is not already in use if (userRepository.existsByEmail(request.email())) { throw new EmailAlreadyExistsException("Email already registered"); } // Create user entity with hashed password User user = new User(); user.setFirstName(request.firstName()); user.setLastName(request.lastName()); user.setEmail(request.email()); user.setPassword(passwordEncoder.encode(request.password())); user.setRole(Role.USER); // Persist the user userRepository.save(user); // Generate and return JWT token String jwtToken = jwtService.generateToken(user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Authenticate an existing user public AuthenticationResponse authenticate(AuthenticationRequest request) { // Delegate verification to AuthenticationManager authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.email(), request.password() ) ); // Load user (authentication succeeded) User user = userRepository.findByEmail(request.email()) .orElseThrow(() -> new UsernameNotFoundException("User not found")); // Generate token with additional claims Map claims = new HashMap<>(); claims.put("role", user.getRole().name()); claims.put("userId", user.getId()); String jwtToken = jwtService.generateToken(claims, user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Refresh token (extend session) public AuthenticationResponse refreshToken(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new InvalidTokenException("Invalid token"); } String oldToken = authHeader.substring(7); String userEmail = jwtService.extractUsername(oldToken); User user = userRepository.findByEmail(userEmail) .orElseThrow(() -> new UsernameNotFoundException("User not found")); // Generate new token String newToken = jwtService.generateToken(user); return new AuthenticationResponse(newToken, jwtExpiration); } } ``` `AuthenticationManager` centralizuje weryfikację poświadczeń i pozwala łatwo zmienić strategię uwierzytelniania. > **Refresh Token** > > Dla większego bezpieczeństwa warto wdrożyć system refresh tokenów z dłuższą ważnością, przechowywanych w bazie danych i unieważnialnych. ## Ochrona endpointów za pomocą adnotacji Bezpieczeństwo na poziomie metody zapewnia drobnoziarnistą kontrolę autoryzacji bezpośrednio w kodzie biznesowym. ```java // UserController.java @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; // Accessible to all authenticated users @GetMapping("/me") public ResponseEntity getCurrentUser( @AuthenticationPrincipal User currentUser ) { return ResponseEntity.ok(UserDTO.from(currentUser)); } // Only the concerned user or an admin can modify the profile @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") @PutMapping("/{id}") public ResponseEntity updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request ) { return ResponseEntity.ok(userService.updateUser(id, request)); } // Reserved for administrators @PreAuthorize("hasRole('ADMIN')") @GetMapping public ResponseEntity> getAllUsers() { return ResponseEntity.ok(userService.findAll()); } // Deletion with post-execution verification @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{id}") public ResponseEntity deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } } ``` ```java // AdminController.java @RestController @RequestMapping("/api/admin") @PreAuthorize("hasRole('ADMIN')") // All methods require ADMIN public class AdminController { private final AdminService adminService; @GetMapping("/dashboard") public ResponseEntity getDashboard() { return ResponseEntity.ok(adminService.getDashboardStats()); } @PostMapping("/users/{id}/role") public ResponseEntity changeUserRole( @PathVariable Long id, @RequestParam Role newRole ) { return ResponseEntity.ok(adminService.changeUserRole(id, newRole)); } } ``` Adnotacja `@AuthenticationPrincipal` wstrzykuje uwierzytelnionego użytkownika bezpośrednio i pozwala uniknąć ręcznego dostępu do `SecurityContext`. ## Obsługa błędów uwierzytelniania Scentralizowana obsługa błędów zapewnia spójne i czytelne odpowiedzi, gdy uwierzytelnianie się nie powiedzie. ```java // SecurityExceptionHandler.java @RestControllerAdvice public class SecurityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(SecurityExceptionHandler.class); // Authentication error (incorrect credentials) @ExceptionHandler(BadCredentialsException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleBadCredentials(BadCredentialsException ex) { return new ErrorResponse( "INVALID_CREDENTIALS", "Invalid email or password", null ); } // Access denied (authenticated but not authorized) @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ErrorResponse handleAccessDenied(AccessDeniedException ex) { return new ErrorResponse( "ACCESS_DENIED", "Access to this resource is not authorized", null ); } // Expired JWT token @ExceptionHandler(ExpiredJwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleExpiredToken(ExpiredJwtException ex) { return new ErrorResponse( "TOKEN_EXPIRED", "Session expired, please log in again", null ); } // Invalid JWT token @ExceptionHandler(JwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleInvalidToken(JwtException ex) { log.warn("Invalid JWT token: {}", ex.getMessage()); return new ErrorResponse( "INVALID_TOKEN", "Invalid authentication token", null ); } // Email already in use during registration @ExceptionHandler(EmailAlreadyExistsException.class) @ResponseStatus(HttpStatus.CONFLICT) public ErrorResponse handleEmailExists(EmailAlreadyExistsException ex) { return new ErrorResponse( "EMAIL_EXISTS", ex.getMessage(), null ); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details ) {} ``` Ujednolicone kody błędów ułatwiają obsługę po stronie klienta i debugowanie. ## Konfiguracja application.yml Wyniesiona konfiguracja pozwala dostosować parametry JWT pod konkretne środowisko. ```yaml # application.yml app: jwt: # Base64 secret key (256 bits minimum for HS256) # Generate with: openssl rand -base64 32 secret: ${JWT_SECRET:yourSuperSecretKeyOf256BitsMinimum} # Validity duration in milliseconds (24 hours) expiration: 86400000 spring: datasource: url: jdbc:postgresql://localhost:5432/springjwt username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres} jpa: hibernate: ddl-auto: validate show-sql: false ``` ```yaml # application-dev.yml app: jwt: # Short expiration for development (1 hour) expiration: 3600000 spring: jpa: show-sql: true hibernate: ddl-auto: update logging: level: org.springframework.security: DEBUG ``` W środowisku produkcyjnym klucz tajny powinien pochodzić ze zmiennej środowiskowej lub usługi zarządzania sekretami, takiej jak Vault. ## Testy integracyjne Testy weryfikują pełne zachowanie systemu uwierzytelniania — od rejestracji aż po dostęp do chronionych zasobów. ```java // AuthenticationIntegrationTest.java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = Replace.ANY) class AuthenticationIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test void shouldRegisterAndAuthenticateUser() { // Registration RegisterRequest registerRequest = new RegisterRequest( "John", "Doe", "john@example.com", "password123" ); ResponseEntity registerResponse = restTemplate .postForEntity("/api/auth/register", registerRequest, AuthenticationResponse.class); assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(registerResponse.getBody().token()).isNotBlank(); // Login with same credentials AuthenticationRequest loginRequest = new AuthenticationRequest( "john@example.com", "password123" ); ResponseEntity loginResponse = restTemplate .postForEntity("/api/auth/login", loginRequest, AuthenticationResponse.class); assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(loginResponse.getBody().token()).isNotBlank(); } @Test void shouldRejectInvalidCredentials() { AuthenticationRequest request = new AuthenticationRequest( "unknown@example.com", "wrongpassword" ); ResponseEntity response = restTemplate .postForEntity("/api/auth/login", request, ErrorResponse.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void shouldAccessProtectedResourceWithValidToken() { // Register to get a token RegisterRequest registerRequest = new RegisterRequest( "Jane", "Doe", "jane@example.com", "password123" ); AuthenticationResponse authResponse = restTemplate .postForObject("/api/auth/register", registerRequest, AuthenticationResponse.class); // Access protected resource with token HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(authResponse.token()); ResponseEntity response = restTemplate.exchange( "/api/users/me", HttpMethod.GET, new HttpEntity<>(headers), UserDTO.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().email()).isEqualTo("jane@example.com"); } @Test void shouldRejectAccessWithoutToken() { ResponseEntity response = restTemplate .getForEntity("/api/users/me", Void.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } } ``` Testy obejmują główne scenariusze: rejestrację, logowanie, dostęp autoryzowany oraz dostęp odrzucony. ## Podsumowanie Wdrożenie uwierzytelniania JWT w Spring Security 6 podlega dobrze zdefiniowanemu schematowi. Serwis JWT odpowiada za generowanie i walidację tokenów, filtr przechwytuje żądania, aby ustanowić kontekst bezpieczeństwa, a konfiguracja definiuje reguły dostępu. **Lista kontrolna wdrożenia:** - ✅ Klucz tajny minimum 256 bitów przechowywany w zmiennej środowiskowej - ✅ HTTPS obowiązkowy w produkcji, aby chronić tokeny w trakcie transmisji - ✅ Czas ważności tokena dostosowany do kontekstu (15 min – 24 h) - ✅ Refresh token dla długich sesji bez ponownego uwierzytelnienia - ✅ Ujednolicona obsługa błędów z kodami biznesowymi - ✅ Logowanie nieudanych prób uwierzytelnienia - ✅ Testy integracyjne pokrywające scenariusze uwierzytelniania - ✅ Rate limiting na endpointach uwierzytelniania Taka architektura stanowi solidną podstawę do zabezpieczania API REST i pozostaje rozszerzalna na bardziej złożone potrzeby, takie jak uwierzytelnianie wieloskładnikowe czy integracja z OAuth2. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/spring-boot/spring-security-6-jwt-authentication