MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Java Spring Boot + React E-Learning Rebuild

1

Part 1: Introduction & Architecture: Monolith to Decoupled Enterprise

2

Part 2: Production Readiness & Security Hardening: Path Traversal, Refresh Tokens & Docker Resilience

3

Part 3: Enterprise Security Architecture: Stateless JWT, Method-Level RBAC & OWASP Top 10 Defense

4

Part 4: Single VPS Production Deployment: Docker Compose, Host Nginx Reverse Proxy & SSL Let's Encrypt

This article is available in Indonesian

🇮🇩 Baca dalam Bahasa Indonesia
🇬🇧 English📚 Java Spring Boot + React E-Learning Rebuild

Java Spring Boot 3.4 & React 19: Enterprise Security Architecture – Stateless JWT, Method-Level RBAC & OWASP Top 10 Defense

A comprehensive security deep dive for the Java 21 Spring Boot 3.4 & React 19 platform: JJWT 0.12 cryptographic signature validation, declarative @PreAuthorize method-level RBAC, Bucket4j token bucket rate limiting against brute-force attacks, and complete OWASP Top 10 mitigation strategies.

Muhammad Fari MadyanAuthor

13 min read

·

Oct 14, 2026


The Paradigm Shift: Securing Decoupled Enterprise Systems

When transitioning from a server-rendered monolith (such as Laravel with Blade or traditional Spring MVC with Thymeleaf) to a decoupled Spring Boot 3.4 REST API and React 19 Single Page Application (SPA), the security architecture fundamentally shifts.

In a traditional monolith:

  • The browser stores a session cookie (JSESSIONID or laravel_session).
  • The server maintains stateful session objects in memory or Redis.
  • Every state mutation relies on CSRF tokens embedded within HTML forms.

In a high-concurrency, multi-client academic platform (open-sourced at mfarim/spring-elearning-react):

  1. Stateless Scalability: The application backend must run across multiple Docker containers behind reverse proxies without relying on sticky sessions or shared session state.
  2. Multi-Client Consistency: The same API serves web browsers, mobile apps (React Native / iOS / Android), and CBT lockdown browser environments uniformly.
  3. Defense-in-Depth: Coarse-grained URL path matchers (requestMatchers("/api/v1/admin/**")) are insufficient. Academic institutions require granular, method-level authorization with fine-grained role evaluation at compile-time and runtime.
  4. Brute-Force & Flood Resilience: Public authentication and high-stakes examination endpoints must be shielded against automated credential stuffing, distributed denial of service, and rapid network retries.

In this deep dive, we break down the production security architecture implemented in mfarim/spring-elearning-react, covering the JJWT 0.12 stateless token engine, declarative @PreAuthorize method-level RBAC, Bucket4j token bucket rate limiting, and a complete OWASP Top 10 mitigation matrix.


1. The Modern Stateless JWT Engine (JJWT 0.12)

Many Java applications still use deprecated JJWT APIs (Jwts.parser().setSigningKey(...)) that lack modern compile-time type safety and cryptographic enforcement. In this project, we implement the modern JJWT 0.12 fluent API.

┌─────────────────────────────────────────────────────────────────────────────┐ │ STATELESS JWT AUTHENTICATION FLOW │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ [ React 19 Client ] │ │ │ │ │ │ 1. POST /api/v1/auth/login { email, password } │ │ ▼ │ │ [ RateLimitingFilter (Bucket4j: 10 req/min/IP) ] │ │ │ │ │ │ 2. Passes Rate Limit Check │ │ ▼ │ │ [ DaoAuthenticationProvider ] │ │ │ • BCryptPasswordEncoder(12) matches hash │ │ ▼ │ │ [ JwtProvider ] │ │ │ • Sign Access Token (15 min) with HMAC-SHA256 (>=256-bit key) │ │ │ • Sign Refresh Token (7 days) with HMAC-SHA256 │ │ ▼ │ │ [ AuthResponse { accessToken, refreshToken, roles, user } ] │ │ │ │ │ │ 3. Subsequent Requests: Header "Authorization: Bearer <token>" │ │ ▼ │ │ [ JwtAuthenticationFilter (OncePerRequestFilter) ] │ │ │ • Parse & Validate Claims with parser().verifyWith(secretKey) │ │ │ • Extract userId, roles, and impersonatorAdminId │ │ │ • Populate SecurityContextHolder with UserPrincipal │ │ ▼ │ │ [ Controller Layer (@PreAuthorize Evaluated via SpEL) ] │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

Cryptographic Key Enforcement & Token Issuance

In backend/src/main/java/com/elearning/security/JwtProvider.java, the constructor enforces strict cryptographic standards upon application bootstrap. Weak keys are rejected immediately to prevent forged token vulnerabilities:

JAVA

// backend/src/main/java/com/elearning/security/JwtProvider.java
@Component
public class JwtProvider {

    private final SecretKey secretKey;
    private final long accessTokenExpirationMs;
    private final long refreshTokenExpirationMs;

    public JwtProvider(
            @Value("${app.jwt.secret}") String secret,
            @Value("${app.jwt.access-token-expiration-ms:900000}") long accessTokenExpirationMs,
            @Value("${app.jwt.refresh-token-expiration-ms:604800000}") long refreshTokenExpirationMs
    ) {
        if (secret == null || secret.trim().isEmpty()) {
            throw new IllegalStateException("CRITICAL: app.jwt.secret is not configured! Please provide JWT_SECRET environment variable.");
        }
        byte[] keyBytes = Decoders.BASE64.decode(secret.trim());
        
        // Cryptographic Enforcement: Must be at least 256 bits (32 bytes) for HMAC-SHA256
        if (keyBytes.length < 32) {
            throw new IllegalArgumentException("CRITICAL: JWT secret key must be at least 256 bits (32 bytes) long for HMAC-SHA256!");
        }
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
        this.accessTokenExpirationMs = accessTokenExpirationMs; // 15 minutes default
        this.refreshTokenExpirationMs = refreshTokenExpirationMs; // 7 days default
    }

    public String generateAccessToken(UserPrincipal userPrincipal) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + accessTokenExpirationMs);

        List<String> roles = userPrincipal.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        var builder = Jwts.builder()
                .subject(userPrincipal.getId().toString())
                .claim("email", userPrincipal.getEmail())
                .claim("name", userPrincipal.getName())
                .claim("roles", roles)
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(secretKey);

        // Append administrative impersonator audit claim if active
        if (userPrincipal.getImpersonatorAdminId() != null) {
            builder.claim("impersonatorAdminId", userPrincipal.getImpersonatorAdminId());
        }

        return builder.compact();
    }
}

The Inspection Pipeline: JwtAuthenticationFilter

Every incoming HTTP request traverses JwtAuthenticationFilter, which extends Spring's OncePerRequestFilter:

JAVA

// backend/src/main/java/com/elearning/security/JwtAuthenticationFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;
    private final CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        String token = getJwtFromRequest(request);

        if (StringUtils.hasText(token) && jwtProvider.validateToken(token)) {
            Long userId = jwtProvider.getUserIdFromToken(token);
            Long impersonatorAdminId = jwtProvider.getImpersonatorAdminIdFromToken(token);

            // Load immutable user principal including current authority grants
            UserDetails userDetails = userDetailsService.loadUserById(userId, impersonatorAdminId);
            
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // Establish security context for downstream thread execution
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

2. Method-Level Role-Based Access Control (RBAC) & SpEL

Many teams configure security exclusively at the HTTP routing level inside SecurityConfig.java:

JAVA

// Coarse-grained URL matching (Necessary, but NOT sufficient on its own)
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/v1/teacher/**").hasAnyRole("ADMIN", "TEACHER")
    .requestMatchers("/api/v1/student/**").hasAnyRole("ADMIN", "STUDENT")
    .anyRequest().authenticated()
);

While URL matchers establish an outer boundary, relying solely on them has severe pitfalls:

  1. Endpoint Refactoring Risk: If a developer changes /api/v1/teacher/exams to /api/v1/exams, an unintentional authorization bypass occurs if route patterns are omitted in SecurityConfig.
  2. No Contextual Logic: URL matchers cannot evaluate method arguments (e.g., verifying that a teacher owns the specific exam ID being modified).
  3. No Domain-Driven Granularity: Actions like publishing a draft exam or updating a question item belong to the same base resource /api/v1/exams/{id}, but may require distinct role permissions.

Enabling Method Security

In Spring Boot 3 / Spring Security 6, we enable declarative method security by adding @EnableMethodSecurity to SecurityConfig.java:

JAVA

// backend/src/main/java/com/elearning/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // Activates @PreAuthorize and @PostAuthorize
@RequiredArgsConstructor
public class SecurityConfig {
    // ...
}

Authority Mapping in UserPrincipal

Spring Security expects authorities formatted with the ROLE_ prefix when evaluating hasRole(...) expressions. In UserPrincipal.java, user roles are mapped cleanly to SimpleGrantedAuthority:

JAVA

// backend/src/main/java/com/elearning/security/UserPrincipal.java
public static UserPrincipal create(User user, Long impersonatorAdminId) {
    Collection<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority(role.getName().name())) // e.g. "ROLE_ADMIN", "ROLE_TEACHER"
            .collect(Collectors.toList());

    return new UserPrincipal(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getPassword(),
            user.isActive(),
            authorities,
            impersonatorAdminId
    );
}

Granular Controller Declarations with SpEL

Now, each controller method declares its exact authorization contract via Spring Expression Language (SpEL):

JAVA

// backend/src/main/java/com/elearning/modules/exam/controller/ExamController.java
@RestController
@RequestMapping("/api/v1/exams")
@RequiredArgsConstructor
public class ExamController {

    private final ExamService examService;

    // Both Students and Teachers can list accessible exams, but service filters by enrollment
    @GetMapping
    public ResponseEntity<ApiResponse<List<ExamResponse>>> getExams(
            @AuthenticationPrincipal UserPrincipal principal) {
        return ResponseEntity.ok(ApiResponse.success(examService.getExamsForUser(principal), "Exams fetched"));
    }

    // Only Teachers and Admins can create new examinations
    @PostMapping
    @PreAuthorize("hasAnyRole('ADMIN', 'TEACHER')")
    public ResponseEntity<ApiResponse<ExamResponse>> createExam(
            @Valid @RequestBody ExamDto dto,
            @AuthenticationPrincipal UserPrincipal principal) {
        ExamResponse response = examService.createExam(dto, principal);
        return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(response, "Exam created"));
    }

    // Updating exam metadata is restricted to Teachers and Admins
    @PutMapping("/{id}")
    @PreAuthorize("hasAnyRole('ADMIN', 'TEACHER')")
    public ResponseEntity<ApiResponse<ExamResponse>> updateExam(
            @PathVariable Long id,
            @Valid @RequestBody ExamDto dto,
            @AuthenticationPrincipal UserPrincipal principal) {
        return ResponseEntity.ok(ApiResponse.success(examService.updateExam(id, dto, principal), "Exam updated"));
    }

    // Question bank manipulation restricted to faculty
    @PostMapping("/{id}/questions")
    @PreAuthorize("hasAnyRole('ADMIN', 'TEACHER')")
    public ResponseEntity<ApiResponse<QuestionResponse>> addQuestion(
            @PathVariable Long id,
            @Valid @RequestBody QuestionDto dto,
            @AuthenticationPrincipal UserPrincipal principal) {
        return ResponseEntity.status(HttpStatus.CREATED).body(
                ApiResponse.success(examService.addQuestion(id, dto, principal), "Question added")
        );
    }
}

Preventing Insecure Direct Object References (IDOR)

Method-level security handles the role check. However, an authorized teacher must not be allowed to delete another teacher's exam!

To prevent IDOR (OWASP A01), the service layer enforces resource ownership validation using the injected UserPrincipal:

JAVA

// backend/src/main/java/com/elearning/modules/exam/service/ExamService.java
@Transactional
public void deleteExam(Long id, UserPrincipal principal) {
    Examination exam = examRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Examination not found with id: " + id));

    // Admin can delete any exam; Teachers can only delete their own exams
    boolean isAdmin = principal.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    boolean isOwner = exam.getTeacher().getId().equals(principal.getId());

    if (!isAdmin && !isOwner) {
        throw new SecurityException("Forbidden: You do not have permission to delete an examination created by another teacher.");
    }

    examRepository.delete(exam);
}

3. Defense Against Brute-Force & Flooding: Bucket4j Token Bucket

Authentication endpoints (/api/v1/auth/login) and CBT submission endpoints (/api/v1/exams/{id}/submit) are the most targeted attack surfaces. Without server-side rate limiting, attackers can launch credential stuffing dictionaries or flood exam grading queues.

In spring-elearning-react, we engineer a multi-tiered server-side rate limiting filter utilizing Bucket4j:

JAVA

// backend/src/main/java/com/elearning/security/RateLimitingFilter.java
@Component
@Slf4j
public class RateLimitingFilter extends OncePerRequestFilter {

    private final Map<String, Bucket> authIpBuckets = new ConcurrentHashMap<>();
    private final Map<String, Bucket> submitIpBuckets = new ConcurrentHashMap<>();
    private final Map<String, Bucket> generalIpBuckets = new ConcurrentHashMap<>();

    // Multi-Tier Rate Limit Capacities
    private static final int AUTH_CAPACITY = 10;       // 10 login/auth requests per minute per IP
    private static final int SUBMIT_CAPACITY = 60;     // 60 autosave/submit requests per minute per IP
    private static final int GENERAL_CAPACITY = 120;   // 120 general API requests per minute per IP

    private Bucket createBucket(int capacity, Duration duration) {
        Bandwidth limit = Bandwidth.classic(capacity, Refill.greedy(capacity, duration));
        return Bucket.builder().addLimit(limit).build();
    }

    @Override
    protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
        String path = request.getRequestURI();
        // Skip rate limiting for static documentation, health probes, and preflight CORS
        return "OPTIONS".equalsIgnoreCase(request.getMethod())
                || path.startsWith("/actuator")
                || path.startsWith("/swagger-ui")
                || path.startsWith("/v3/api-docs")
                || path.startsWith("/ws");
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        String clientIp = LoginRateLimiterService.getClientIp(request);
        String requestUri = request.getRequestURI();

        // Tier 1: Strictest limit on Auth endpoints (Login / Register)
        if (requestUri.startsWith("/api/v1/auth/login") || requestUri.startsWith("/api/v1/auth/register")) {
            Bucket authBucket = authIpBuckets.computeIfAbsent(
                    clientIp, k -> createBucket(AUTH_CAPACITY, Duration.ofMinutes(1))
            );
            if (!authBucket.tryConsume(1)) {
                log.warn("Rate limit [AUTH] exceeded for IP {} on {}", clientIp, requestUri);
                sendRateLimitResponse(response, "Too many authentication attempts. Please wait 1 minute before retrying.");
                return;
            }
        }

        // Tier 2: Anti-flood protection for exam autosave and submissions
        else if (requestUri.contains("/exams/") && (requestUri.endsWith("/submit") || requestUri.endsWith("/answer"))) {
            Bucket submitBucket = submitIpBuckets.computeIfAbsent(
                    clientIp, k -> createBucket(SUBMIT_CAPACITY, Duration.ofMinutes(1))
            );
            if (!submitBucket.tryConsume(1)) {
                log.warn("Rate limit [EXAM SUBMIT] exceeded for IP {} on {}", clientIp, requestUri);
                sendRateLimitResponse(response, "High exam submission velocity detected. Please wait a moment.");
                return;
            }
        }

        // Tier 3: General API tier for all other endpoints
        else if (requestUri.startsWith("/api/v1")) {
            Bucket generalBucket = generalIpBuckets.computeIfAbsent(
                    clientIp, k -> createBucket(GENERAL_CAPACITY, Duration.ofMinutes(1))
            );
            if (!generalBucket.tryConsume(1)) {
                log.warn("Rate limit [GENERAL API] exceeded for IP {} on {}", clientIp, requestUri);
                sendRateLimitResponse(response, "API request rate limit exceeded. Please throttle your requests.");
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    private void sendRateLimitResponse(HttpServletResponse response, String message) throws IOException {
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); // HTTP 429
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Retry-After", "60");
        response.setHeader("X-RateLimit-Retry-After-Seconds", "60");

        String jsonPayload = String.format("{\"success\":false,\"message\":\"%s\",\"data\":null}", message);
        response.getWriter().write(jsonPayload);
        response.getWriter().flush();
    }
}

The Filter Chain Ordering Trap

A common pitfall in Spring Security is registering a filter as both a Spring @Component and within HttpSecurity.addFilterBefore(...). When this happens, the servlet container executes the filter twice per request!

In SecurityConfig.java, we solve this cleanly by:

  1. Disabling the default servlet registration via FilterRegistrationBean.
  2. Explicitly placing RateLimitingFilter before LogoutFilter.class so excessive requests are rejected before allocating database or crypto resources:

JAVA

// backend/src/main/java/com/elearning/config/SecurityConfig.java
http
    .addFilterBefore(rateLimitingFilter, LogoutFilter.class)
    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

@Bean
public FilterRegistrationBean<RateLimitingFilter> rateLimitingFilterRegistration(RateLimitingFilter filter) {
    FilterRegistrationBean<RateLimitingFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false); // Prevents duplicate execution by Servlet container
    return registration;
}

4. The 1-Click Administrative Impersonation Engine

In high-stakes academic environments, school administrators frequently need to diagnose why a student's examination attempt is malfunctioning. In a session-based monolith, admins call Auth::loginUsingId(). In a stateless JWT architecture, how do you impersonate a user securely without compromising password hashes or session integrity?

In AuthController.java and AuthService.java, we implement an audit-safe Impersonation Engine:

JAVA

// backend/src/main/java/com/elearning/modules/auth/controller/AuthController.java
@PostMapping("/admin/impersonate/{userId}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Start Impersonation", description = "Admin can impersonate a teacher or student user")
public ResponseEntity<ApiResponse<AuthResponse>> impersonate(
        @PathVariable Long userId,
        @AuthenticationPrincipal UserPrincipal adminPrincipal
) {
    AuthResponse response = authService.impersonate(userId, adminPrincipal);
    return ResponseEntity.ok(ApiResponse.ok(response, "Impersonation started successfully"));
}

@PostMapping("/admin/stop-impersonate")
@Operation(summary = "Stop Impersonation", description = "Revert impersonated session back to administrator")
public ResponseEntity<ApiResponse<AuthResponse>> stopImpersonate(
        @AuthenticationPrincipal UserPrincipal currentPrincipal
) {
    AuthResponse response = authService.stopImpersonate(currentPrincipal);
    return ResponseEntity.ok(ApiResponse.ok(response, "Returned to admin session"));
}

How the Token Preserves Audit Integrity

When authService.impersonate(userId, adminPrincipal) runs:

  1. It validates that the target user exists and is active.
  2. It generates a valid access token whose subject is the target student's ID and whose roles match the target student's permissions.
  3. Crucially, it attaches an impersonatorAdminId claim containing the administrator's ID.
  4. When stopImpersonate is called, the backend reads currentPrincipal.getImpersonatorAdminId(), verifies the original admin account, and seamlessly issues a fresh admin token.

No passwords are ever exposed, and all mutations performed while impersonating carry the admin audit ID in server logs!


5. Enterprise OWASP Top 10 Security Mapping

Here is the complete architectural mapping showing how each vulnerability in the OWASP Top 10 (2021) standard is directly neutralized across the Java Spring Boot + React codebase:

OWASP VulnerabilityRisk ScenarioEngineering Mitigation in spring-elearning-react
A01: Broken Access ControlUnauthorized student modifying grades, reading other students' exams (IDOR), or path traversal.• Declarative @PreAuthorize("hasAnyRole(...)") on controllers.
• Resource ownership verification in service layer.
• Strict .startsWith(uploadDir) path traversal boundaries in FileStorageService.
A02: Cryptographic FailuresWeak password storage or compromised token signing keys.• BCryptPasswordEncoder with strength factor 12.
• Mandatory 256-bit (32-byte) key validation in JwtProvider for HMAC-SHA256.
• HTTPS enforced at Host Nginx reverse proxy.
A03: InjectionSQL injection in search bars, question filters, or student rosters.• 100% Parameterized queries via Spring Data JPA and Hibernate.
• Zero raw SQL string concatenations.
• Input type binding via strong DTOs.
A04: Insecure DesignStudent manipulating local computer clock to gain exam time or bypassing submission rules.• Server-authoritative timer logic calculated from database start timestamp (calculateRemainingSeconds).
• Tab switch blur detection tracked server-side.
A05: Security MisconfigurationLeaking database SQL dialect or stack traces to public API callers.• GlobalExceptionHandler masks internal SQL exceptions with generic messages (CWE-209 mitigation).
• Actuator endpoints restricted to /actuator/health and /actuator/info.
A06: Vulnerable ComponentsKnown CVEs in outdated Spring Boot or transitive Maven libraries.• Modern baseline on Java 21 LTS and Spring Boot 3.4.3.
• Continuous dependency scanning with Dependabot and Maven security plugins.
A07: Identification FailuresCredential stuffing, brute-force password guessing, session hijacking.• Multi-tier Bucket4j RateLimitingFilter (10 req/min/IP).
• Refresh token rotation engine.
• Short-lived 15-minute access token window.
A08: Software & Data IntegrityDeserialization of untrusted payloads or invalid entity schemas.• Strict input validation via jakarta.validation (@Valid, @NotBlank, @Size, @Email).
• DTO immutability via Java records and Lombok builders.
A09: Security Logging & MonitoringSilent failures during security breaches or lack of traceability.• SLF4J structured logging for all security exceptions.
• Impersonator audit trail embedded in JWT claims.
• Actuator health probes for container orchestrator readiness.
A10: Server-Side Request Forgery (SSRF)Attacker coercing the server to fetch malicious remote URLs.• Static file storage sandbox with isolated internal storage locations.
• No unvalidated external URL fetching permitted in upload endpoints.

Conclusion & What's Next

Enterprise security is not achieved through superficial plugins or haphazard code snippets—it demands a cohesive, layered architecture.

By combining the JJWT 0.12 modern builder engine, declarative @PreAuthorize method security, Bucket4j token bucket rate limiting, and defense-in-depth OWASP Top 10 mitigation, the Spring E-Learning platform demonstrates how a modern Java 21 & React 19 application achieves banking-grade resilience while remaining responsive under high-concurrency academic workloads.

In Part 4 (Final), we conclude this series with the ultimate operational milestone: Single VPS Production Deployment: Docker Compose, Host Nginx Reverse Proxy & SSL Let's Encrypt.

Continue Reading

Previous article

← Previous Article

Java Spring Boot + React E-Learning: Production Readiness, Security Hardening & Zero-Downtime Resilience (PR #1 Breakdown)

Next Article →

Java Spring Boot + React E-Learning: Single VPS Production Deployment with Docker Compose, Host Nginx Reverse Proxy & SSL Let's Encrypt

Next article