C
Spring Boot/Operations/Lesson 10

Operations Core — @Transactional · Spring Security

60 min·theory

Operations Core — @Transactional · Spring Security

🎯 What you'll be able to do after this lesson

After finishing this lesson, you'll be able to confidently do the following 3 things.

  • ✅ Spring Security Filter Chain diagram + JWT authentication filter position
  • ✅ Method-level authorization control with @PreAuthorize
  • ✅ Why BCryptPasswordEncoder is the standard (MD5/SHA-1 are forbidden)

Keep these learning objectives as a checklist — close the lesson only when you can answer all of them.

@Transactional — *Spring's Most Powerful Single Line*

The Core Line

A single @Transactional annotation wraps that method in a DB transaction. Automatically rolls back on exception, commits on success. The standard for operations where atomicity matters, such as wire transfers and payments.

Simplest Usage

java
@Service
public class TransferService {
    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).get();
        Account to   = accountRepo.findById(toId).get();
        from.withdraw(amount);
        to.deposit(amount);
        // End of method → commit
        // Exception → automatic rollback
    }
}

Spring automatically wraps the DB's BEGIN, COMMIT, and ROLLBACK for you. No more forgetting.

Proxy-Based — Only Applied on External Calls

Spring's @Transactional works via AOP proxy. The transaction is only applied when the method is called from outside.

java
@Service
public class OrderService {
    @Transactional
    public void create() {
        process();        // ❌ Internal call — transaction NOT applied!
    }

    @Transactional
    public void process() { ... }
}

Calling a method within the same class bypasses the proxy → annotation is ignored.

Solutions:

  • Extract to a separate Bean
  • Or call the self-proxy via AopContext.currentProxy()

Propagation — Transaction Within a Transaction

java
@Service
public class OrderService {
    @Transactional
    public void create() {
        logService.save();     // What if logService also has @Transactional?
    }
}

You're already inside a transaction, and you call another @Transactional method — what happens? It's determined by the propagation option.

OptionMeaning
REQUIRED (default)Joins an existing transaction, or starts a new one if none exists
REQUIRES_NEWAlways starts a new transaction (suspends the existing one)
NESTEDCreates a savepoint inside the existing transaction — partial rollback is possible
MANDATORYRequires an existing transaction (throws error if none)
SUPPORTSJoins an existing transaction, or runs without a transaction if none exists
NEVERThrows an error if an existing transaction is present

Most common usage: the default REQUIRED. However, for log and audit records that must be persisted even if the parent rolls back, use REQUIRES_NEW.

java
@Transactional(propagation = REQUIRES_NEW)
public void writeAuditLog(...) { ... }

Rollback Rules — Only RuntimeException Is Automatic

java
@Transactional
public void process() {
    riskyOperation();      // throws IOException (Checked)
}

Default: Only RuntimeException and its descendants (Unchecked) trigger automatic rollback. Checked exceptions like IOException result in a commit.

This is a Java-specific quirk — the distinction barely exists in other languages. Without knowing this, you can easily cause data corruption incidents.

Solution:

java
@Transactional(rollbackFor = Exception.class)
public void process() throws IOException { ... }

Rollback on all exceptions. This is usually the safe default. Kotlin and modern Spring code almost always make this explicit.

readOnly = true — Read-Only Optimization

java
@Transactional(readOnly = true)
public List<User> findAll() { ... }

For methods that only read data, use readOnly=true. JPA skips dirty checking, improving performance. Hibernate FlushMode is also automatically set to NEVER.

Class-level:

java
@Service
@Transactional(readOnly = true)        // Default is readOnly
public class UserService {
    public User findById(Long id) { ... }

    @Transactional                      // Override only for write methods
    public User update(Long id, ...) { ... }
}

This naturally aligns with the CQRS pattern.

5 Common Pitfalls

1. Internal call — Explained above. Method calls within the same class bypass the transaction.
2. Checked exception commit — Specify rollbackFor = Exception.class
3. Private method — Proxy doesn't apply. Must be public.
4. External API call inside a transaction — Holds a DB lock for as long as the external response takes. Recommended to call outside the transaction.
5. Transaction scope too large — Wrapping the entire method holds locks too long. Keep to minimum scope.

Summary

  • @Transactional enables automatic transactions with one line
  • Proxy-based → only applied on external calls
  • Control nested behavior with propagation options
  • Default only rolls back on RuntimeException → recommend rollbackFor = Exception.class
  • Read methods should use readOnly = true

Spring Security — *Authentication and Authorization*

Authentication vs. Authorization

Two terms often confused:

  • Authentication = Who are you? Identity verification (login)
  • Authorization = Are you allowed to do this? Permission check (admin page access)

Spring Security handles both.

Minimal Configuration (Spring Security 6 / Spring Boot 3)

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())           // Usually disabled for SPA/API
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

URL-based permissions, session policy, JWT filter registration — all in one chain.

Login Flow — The Most Common Pattern

1. Password Verification (Authentication Manager)

java
@RequiredArgsConstructor
public class LoginService {
    private final UserRepository userRepo;
    private final PasswordEncoder encoder;
    private final JwtProvider jwt;

    public LoginResponse login(String email, String password) {
        User u = userRepo.findByEmail(email)
            .orElseThrow(() -> new BadCredentialsException("Email not found"));

        if (!encoder.matches(password, u.getPassword())) {
            throw new BadCredentialsException("Password mismatch");
        }

        String accessToken  = jwt.createAccess(u.getId(), u.getRole());
        String refreshToken = jwt.createRefresh(u.getId());

        return new LoginResponse(accessToken, refreshToken);
    }
}

Key point: hash comparison of passwords using bcrypt. Never store plain text.

2. JWT Issuance + Response

  • Access Token — 15 minutes, sent in header with every API call
  • Refresh Token — 7 days, stored in httpOnly cookie (XSS protection)

3. Subsequent Requests — JwtAuthenticationFilter

java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwt;

    @Override
    protected void doFilterInternal(HttpServletRequest req, ...) {
        String token = extractToken(req.getHeader("Authorization"));
        if (token != null && jwt.validate(token)) {
            Authentication auth = jwt.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
    }
}

On every request: validate token → store user info in SecurityContext → access via @AuthenticationPrincipal elsewhere.

Method-Level Authorization

java
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void delete(@PathVariable Long id) { ... }

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
@PutMapping("/users/{userId}")
public UserDto update(@PathVariable Long userId, ...) { ... }

Permissions at the method level, not just URL level. SpEL (Spring Expression Language) allows expressing complex rules.

Activate with: @EnableMethodSecurity on your configuration class.

OAuth 2.0 — Social Login

Don't implement it yourself — use Spring Security OAuth2 Client:

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: email,profile
          kakao:
            client-id: ...

Spring handles the entire OAuth flow (redirects, token exchange, user info retrieval). You only map against users in your own DB in the callback.

Common Security Pitfalls

1. Overusing CSRF disable — OK for REST API + JWT. Enable it for session cookie-based setups.
2. CORS too permissive — Never use *. Use an explicit origin list.
3. Storing passwords in plain text — One step from disaster. bcrypt or argon2 is mandatory.
4. JWT secret exposure — Use environment variables. Never hard-code it.
5. SQL Injection — Always use JPA or parameterized queries.

Summary

  • Spring Security = the standard for authentication and authorization
  • JWT is the modern trend (stateless, mobile-friendly)
  • bcrypt for passwords, JWT secret in environment variables, appropriate CSRF and CORS settings
  • Method-level authorization uses @PreAuthorize
  • Social login is handled by OAuth2 Client with one line

Spring Security Filter Chain · JWT Filter Implementation

The Core of Security — The Filter Chain

Spring Security connects dozens of filters in a chain. When an HTTP request comes in, it passes through every filter in order before reaching the controller.

code
[HTTP Request]
    ↓
[SecurityContextPersistenceFilter]   ← Load authentication info
    ↓
[UsernamePasswordAuthenticationFilter]   ← Login form
    ↓
[BearerTokenAuthenticationFilter]   ← JWT (custom)
    ↓
[ExceptionTranslationFilter]   ← Handle auth failures
    ↓
[FilterSecurityInterceptor]   ← Authorization check
    ↓
[DispatcherServlet → Controller]

To add JWT authentication, you need to insert your own filter into the chain.

JWT Filter — Extending OncePerRequestFilter

java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider provider;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                     HttpServletResponse res,
                                     FilterChain chain) throws ServletException, IOException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Authentication auth = provider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException e) {
                log.debug("JWT validation failed: {}", e.getMessage());
            }
        }
        chain.doFilter(req, res);
    }
}

OncePerRequestFilter guarantees exactly one execution per request. Prevents duplicate execution from Spring's forward and include dispatches.

Registering the SecurityFilterChain

java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                 // JWT is stateless → CSRF not needed
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();    // bcrypt — *never use MD5/SHA-1*
    }
}

Use .addFilterBefore(...) to insert the filter at the desired position.

Method-Level Authorization — @PreAuthorize

java
@EnableMethodSecurity   // Add to SecurityConfig

@RestController
class AdminController {

    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/users/{id}")
    public void deleteUser(@PathVariable Long id) { ... }

    @PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
    @GetMapping("/users/{id}")
    public UserResponse get(@PathVariable Long id) { ... }
}

SpEL (Spring Expression Language) lets you express complex authorization conditions. Patterns like "only the user themselves or an ADMIN" can be expressed in a single line.

Summary

  • Filter chain — the request processing pipeline
  • OncePerRequestFilter — the standard base for custom filters like JWT
  • @PreAuthorize — authorization control at the controller and service level
  • BCryptPasswordEncoder — passwords must always use bcrypt

When asked in an interview "How have you applied Security?", you should be able to mention these 4 things.

🤖 Try Asking AI Like This

Knowing the concepts from this lesson lets you give specific instructions to AI. Not a vague 'fix this,' but a request with vocabulary — that's where token savings begin.

  • "Add a JWT authentication filter (OncePerRequestFilter) to this SecurityConfig"
  • "Protect this method with @PreAuthorize('hasRole(ADMIN)')"
  • "Register BCryptPasswordEncoder as a Bean"

Why This Reduces Tokens

Without knowing the concepts, even after receiving an AI response, you have to ask "What is that?" again. Those follow-up questions are what consume tokens. Learn the concepts once, and the conversation ends in a single exchange.

Operations Core — @Transactional · Spring Security - Spring Boot