Back to Spring Security: JWT, OAuth2, CSRF/CORS
Q2Question 2 of 3

What is CSRF vs CORS and Bean Validation?

Key Concepts
  • CSRF tricks the browser into making authenticated requests using the victim's session cookie -- disable it for stateless JWT REST APIs because no cookie is involved.
  • CORS is a browser rule that blocks JavaScript from reading responses of a different origin -- configure Access-Control-Allow-Origin for your frontend domain.
  • Bean Validation with @Valid and JSR-380 annotations (@NotBlank, @Positive, @Email) rejects bad input before it reaches business logic.
  • MethodArgumentNotValidException is caught by @RestControllerAdvice to return structured 400 responses with per-field error messages.
  • EntityManagerFactory creates EntityManager instances that manage the persistence context (first-level cache) for JPA operations.
  • Spring JDBC (JdbcTemplate) gives full SQL control for complex reporting; Spring Data JPA auto-generates CRUD queries from method names.
  • Use JPA for everyday entity CRUD and drop down to JDBC for analytical queries with aggregations, window functions, or multi-table joins.

Definition

CSRF exploits session cookies to forge requests, CORS restricts cross-origin JavaScript calls, @Valid rejects bad input at the controller boundary, and Spring Data JPA handles CRUD while JdbcTemplate handles complex SQL.
  • CSRF (Cross-Site Request Forgery) -- an attack where a malicious site tricks the browser into sending an authenticated request using the victim's session cookie; Spring Security enables CSRF protection by default for session-based apps.
  • CORS (Cross-Origin Resource Sharing) -- a browser security rule that blocks JavaScript from reading responses from a different origin unless the server explicitly allows it via Access-Control-Allow-Origin headers.
  • Bean Validation (JSR-380) -- a standard that validates request DTOs with annotations like @NotBlank, @Positive, and @Email; Spring triggers validation when the controller parameter is annotated with @Valid.
  • EntityManagerFactory -- the JPA factory that creates EntityManager instances, each of which manages a persistence context (first-level cache) for database operations.
  • Spring Data JPA -- a repository abstraction that auto-generates queries from method names, backed by Hibernate as the ORM.
  • Spring JDBC (JdbcTemplate) -- a template class that executes hand-written SQL with row mapping, giving full control over complex queries.

CSRF is like someone forging a withdrawal slip with your account number while you are still logged in at the bank counter -- the teller processes it because your session is active. CORS is the bank's policy that only accepts requests from its own website, not from random third-party sites. Bean Validation is the form checker at National Bank of India who rejects a loan application if the amount field is blank or negative before it reaches the loan officer.

Understanding the Concepts

What is CSRF?

CSRF stands for Cross-Site Request Forgery. It is an attack where a malicious website tricks your browser into making an authenticated request to a different site without your knowledge.

Here is how the attack works. Rajesh logs in to his National Bank of India internet banking portal. The server creates an HTTP session and sends back a session cookie. Now Rajesh opens a second tab and visits a malicious website. That website contains a hidden form that submits a fund transfer request to the bank's server. Because the browser automatically attaches the session cookie to every request going to the bank's domain, the server sees a perfectly authenticated request and processes the transfer. Rajesh never clicked anything.

The classic protection is the synchronizer token pattern. The server generates a random token, embeds it in every real form, and rejects any request that does not carry the matching token. A malicious site cannot read that token because of the browser's same-origin policy.

JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
public class CsrfEnabledConfig {

    @Bean
    public SecurityFilterChain sessionBasedChain(HttpSecurity http) throws Exception {
        // CSRF protection ON (default) for a session-based banking portal
        http.csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
        return http.build();
    }
}

Why is it safe to disable CSRF in stateless REST APIs?

CSRF only works when the browser automatically sends credentials, which means cookies and sessions. A stateless REST API that authenticates with a JWT in the Authorization header is not vulnerable to CSRF. The browser does not automatically attach that header -- the JavaScript must set it explicitly, and a malicious site cannot read the token to set it.

That is why for stateless JWT APIs at National Bank of India, Pankaj (the architect) disables CSRF and sets the session policy to STATELESS.

JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class StatelessSecurityConfig {

    @Bean
    public SecurityFilterChain jwtChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}
TEXT
Expected behavior:
- No CSRF token is required in requests.
- No HttpSession is created on the server.
- Every request must carry a valid JWT in the Authorization header.

What is CORS and how is it different from CSRF?

CORS stands for Cross-Origin Resource Sharing. It is a browser security rule, not an attack. By default, JavaScript running on https://portal.nationalbank.com is not allowed to read responses from https://api.nationalbank.com because they are different origins (different subdomains count as different origins).

When JavaScript makes a cross-origin request, the browser first sends a preflight OPTIONS request asking the server: "Are you willing to accept calls from this origin, with these methods and headers?" The server answers with Access-Control-Allow-Origin and related headers. If the origin is allowed, the real request proceeds. If not, the browser blocks the JavaScript from seeing the response.

| Aspect | CSRF | CORS |
|--------|------|------|
| What it is | An attack that forges authenticated requests | A browser security policy for cross-origin calls |
| Trigger | Malicious site submits a form using victim's cookie | JavaScript calls a different origin |
| Direction | Attacker sends a request TO your server | Browser blocks reading a response FROM another server |
| Protection | Synchronizer token, SameSite cookie attribute | Access-Control-Allow-Origin headers |
| Disable safely? | Yes, for stateless JWT APIs (no cookies) | No, CORS protects users from malicious scripts |

JAVA
package com.bank.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://portal.nationalbank.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Authorization", "Content-Type")
                .maxAge(3600);
    }
}
TEXT
Expected behavior:
- Browser sends OPTIONS preflight to /api/accounts.
- Server responds with Access-Control-Allow-Origin: https://portal.nationalbank.com.
- Browser allows the real GET/POST request to proceed.
- Requests from any other origin are blocked by the browser.

How does Bean Validation work with @Valid?

When Priya submits a loan application through the banking portal, we must reject bad data before it reaches the business logic. Bean Validation (JSR-380) lets us declare constraints on DTO fields with annotations, and Spring triggers validation when we annotate the @RequestBody parameter with @Valid.

When validation fails, Spring throws a MethodArgumentNotValidException. We catch this with a @RestControllerAdvice handler and return a structured 400 response with per-field error messages.

JAVA
package com.bank.dto;

import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;

public class LoanApplicationRequest {

    @NotBlank(message = "Account number is required")
    private String accountNumber;

    @Positive(message = "Loan amount must be greater than zero")
    private int tenureMonths;

    @DecimalMin(value = "10000.00", message = "Minimum loan amount is 10,000")
    private BigDecimal amount;

    // getters and setters
    public String getAccountNumber() { return accountNumber; }
    public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; }
    public int getTenureMonths() { return tenureMonths; }
    public void setTenureMonths(int tenureMonths) { this.tenureMonths = tenureMonths; }
    public BigDecimal getAmount() { return amount; }
    public void setAmount(BigDecimal amount) { this.amount = amount; }
}
JAVA
package com.bank.controller;

import com.bank.dto.LoanApplicationRequest;
import com.bank.service.LoanService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoanController {

    private final LoanService loanService;

    public LoanController(LoanService loanService) {
        this.loanService = loanService;
    }

    @PostMapping("/api/loans")
    public ResponseEntity<String> apply(@Valid @RequestBody LoanApplicationRequest req) {
        String loanId = loanService.processApplication(req);
        return ResponseEntity.ok("Loan approved: " + loanId);
    }
}
JAVA
package com.bank.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .collect(Collectors.toMap(
                        fe -> fe.getField(),
                        fe -> fe.getDefaultMessage(),
                        (a, b) -> a));
        return ResponseEntity.badRequest().body(errors);
    }
}
TEXT
Request: POST /api/loans
{ "accountNumber": "", "tenureMonths": -1, "amount": 500 }

Response: 400 Bad Request
{
  "accountNumber": "Account number is required",
  "tenureMonths": "Loan amount must be greater than zero",
  "amount": "Minimum loan amount is 10,000"
}

What is EntityManagerFactory?

EntityManagerFactory is the JPA factory that creates EntityManager instances. Each EntityManager manages a persistence context, which is a first-level cache that tracks entities loaded during a transaction. When Rajesh calls entityManager.find(Account.class, "SAV2001"), the EntityManager loads the Account into the persistence context. If Rajesh modifies the account's balance, the EntityManager detects the change automatically at flush time (dirty checking) and generates the UPDATE SQL.

Spring Boot auto-configures an EntityManagerFactory backed by Hibernate. You rarely interact with it directly because Spring Data JPA wraps it behind the repository interface.

JAVA
package com.bank.service;

import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class AccountService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void creditInterest(String accountId, BigDecimal interest) {
        Account account = entityManager.find(Account.class, accountId);
        // account is now managed in the persistence context
        account.setBalance(account.getBalance().add(interest));
        // no explicit save needed -- dirty checking generates UPDATE at flush
    }
}
TEXT
Hibernate log:
SELECT * FROM accounts WHERE id = 'SAV2001'
UPDATE accounts SET balance = 150000.00 WHERE id = 'SAV2001'

What is the difference between Spring JDBC and Spring Data JPA?

Spring JDBC gives you JdbcTemplate, where you write the SQL by hand and map rows yourself. It is ideal for complex reporting queries with aggregations, window functions, and multi-table joins. Spring Data JPA gives you a repository interface where you declare method names and Spring generates the query, backed by Hibernate as the ORM. It is ideal for standard CRUD operations.

| Aspect | Spring JDBC (JdbcTemplate) | Spring Data JPA |
|--------|---------------------------|------------------|
| SQL | Hand-written by the developer | Auto-generated from method names |
| ORM | No ORM, raw ResultSet mapping | Hibernate manages entities |
| Caching | No first-level cache | Persistence context cache |
| Best for | Complex reporting, aggregations | Standard CRUD operations |
| Relationships | Manual joins in SQL | @OneToMany, @ManyToOne annotations |
| Learning curve | Lower, just SQL and RowMapper | Higher, need to understand JPA lifecycle |

JAVA
package com.bank.repository;

import com.bank.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

// Spring Data JPA -- no SQL written, queries generated from method names
public interface AccountRepository extends JpaRepository<Account, String> {
    List<Account> findByCustomerIdAndStatus(String customerId, String status);
}
JAVA
package com.bank.repository;

import com.bank.dto.DailySettlementReport;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;

// Spring JDBC -- hand-written SQL for complex reporting
@Repository
public class ReportRepository {

    private final JdbcTemplate jdbcTemplate;

    public ReportRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List<DailySettlementReport> getDailySettlement() {
        return jdbcTemplate.query(
            "SELECT txn_date, COUNT(*) AS txn_count, SUM(amount) AS total "
            + "FROM transactions WHERE status = ? GROUP BY txn_date ORDER BY txn_date",
            (rs, i) -> new DailySettlementReport(
                rs.getDate("txn_date").toLocalDate(),
                rs.getInt("txn_count"),
                rs.getBigDecimal("total")),
            "SETTLED");
    }
}
TEXT
Spring Data JPA generates:
SELECT * FROM accounts WHERE customer_id = ? AND status = ?

Spring JDBC executes your exact SQL:
SELECT txn_date, COUNT(*), SUM(amount) FROM transactions WHERE status = 'SETTLED' GROUP BY txn_date

What are common mistakes with CSRF, CORS, and Validation?

The most common mistake is disabling CSRF on a session-based web application. This leaves the application wide open to forged requests.

JAVA
// WRONG: disabling CSRF on a session-based app
@Bean
public SecurityFilterChain broken(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable()); // dangerous if using sessions
    http.formLogin(Customizer.withDefaults());
    return http.build();
}
JAVA
// CORRECT: disable CSRF only when using stateless JWT
@Bean
public SecurityFilterChain correct(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
    return http.build();
}

Another common mistake is using allowedOrigins("*") in CORS configuration, which allows any website to call your API. Always whitelist specific origins.

With Bean Validation, a common mistake is forgetting @Valid on the @RequestBody parameter. Without it, the validation annotations on the DTO fields are completely ignored.

JAVA
// WRONG: missing @Valid -- validation annotations are ignored
@PostMapping("/api/loans")
public ResponseEntity<String> apply(@RequestBody LoanApplicationRequest req) {
    // req.getAmount() could be null or negative -- no validation happens
    return ResponseEntity.ok(loanService.processApplication(req));
}
JAVA
// CORRECT: @Valid triggers JSR-380 validation
@PostMapping("/api/loans")
public ResponseEntity<String> apply(@Valid @RequestBody LoanApplicationRequest req) {
    return ResponseEntity.ok(loanService.processApplication(req));
}

What should a senior engineer know?

  • SameSite cookie attribute. Modern browsers support SameSite=Strict or SameSite=Lax on cookies, which prevents CSRF by not sending cookies on cross-site requests. This is a defense-in-depth layer alongside CSRF tokens.
  • CORS preflight caching. The maxAge setting in CORS configuration tells the browser how long to cache preflight responses. Setting it to 3600 seconds avoids a preflight OPTIONS call before every real request.
  • Custom validators. JSR-380 supports custom constraint annotations. At National Bank of India, Rajesh creates a @ValidAccountNumber annotation that checks the format and Luhn checksum.
  • Nested validation. If a DTO contains another object (like AddressDto), annotate the nested field with @Valid to trigger cascading validation.
  • JPA vs JDBC coexistence. Both can coexist in the same Spring Boot service. Use JPA repositories for standard entity CRUD and JdbcTemplate for reporting queries that do not map cleanly to entities. Spring auto-configures both when you include the starters.

What are common interview follow-up questions?

  • Can a CSRF attack succeed if the API uses JWT tokens instead of cookies? (No, because the browser does not automatically send the Authorization header.)
  • What is the difference between allowedOrigins("*") and allowedOriginPatterns("*")? (allowedOriginPatterns supports wildcards in subdomains and is required when allowCredentials is true.)
  • What happens if you put @Valid on a @PathVariable? (It does not work for path variables; use @Validated on the class and constraints directly on the parameter.)
  • Can you use JPA and JdbcTemplate in the same transaction? (Yes, they share the same DataSource and connection via Spring's transaction manager.)
  • What is the N+1 problem in JPA? (Loading a list of accounts triggers a separate query for each account's transactions; fix with JOIN FETCH or @EntityGraph.)

Visual Overview

DIAGRAM
  CSRF Attack Flow
  ════════════════
  Priya logs in to National Bank portal
       │
       ▼
  ┌──────────────────────┐      ┌──────────────────────┐
  │  Browser stores      │      │  Malicious site       │
  │  session cookie      │◄─────│  hidden form:         │
  │  for bank.com        │      │  POST /transfer       │
  └──────────┬───────────┘      │  amount=50000         │
             │                  └──────────────────────┘
             │  Browser attaches cookie automatically
             ▼
  ┌──────────────────────┐
  │  Bank server sees    │
  │  valid session       │
  │  → processes transfer│
  └──────────────────────┘

  CORS Preflight Flow
  ═══════════════════
  portal.nationalbank.com        api.nationalbank.com
       │                              │
       │  OPTIONS /api/accounts       │
       │  Origin: portal.national...  │
       │─────────────────────────────►│
       │                              │
       │  Access-Control-Allow-Origin │
       │  portal.nationalbank.com     │
       │◄─────────────────────────────│
       │                              │
       │  GET /api/accounts           │
       │  (real request proceeds)     │
       │─────────────────────────────►│
       │                              │

Key Takeaway

CSRF protection prevents forged requests that exploit session cookies. The attacker tricks the browser into sending a request to the bank while the victim's session is active. The synchronizer token pattern and the SameSite cookie attribute block this attack. For stateless JWT REST APIs, CSRF protection is safely disabled because the browser does not automatically attach the Authorization header. CORS is a separate browser policy that blocks JavaScript from reading responses from a different origin -- configure it to allow only your frontend domain.

Bean Validation with @Valid and JSR-380 annotations checks request DTOs at the controller boundary. Constraints like @NotBlank, @Positive, and @DecimalMin reject bad input before it reaches the business logic. Failures throw MethodArgumentNotValidException, which a @RestControllerAdvice handler maps to structured 400 responses. For data access, Spring Data JPA auto-generates queries from method names and handles CRUD with zero SQL. JdbcTemplate gives full SQL control for complex reporting with aggregations and joins. Both coexist in the same service at National Bank of India -- Rajesh uses JPA for account CRUD and JDBC for the daily settlement report.

Remember one sentence: CSRF off for stateless JWT APIs, CORS configured for your frontend origin, @Valid on every @RequestBody, and JPA for CRUD while JDBC for complex SQL.

---

Share
Use ← → arrow keys to navigate