🎯 18주차 학습 커리큘럼 — Spring Security 완전 정복

Psj·2026년 5월 6일

F-lab

목록 보기
19/240

🎯 18주차 학습 커리큘럼 — Spring Security 완전 정복

17주차(분산 시스템) 이후 Claude가 임의로 구성한 학습 경로.
면접 거의 100% 출제 영역인 Spring Security와 인증/인가를 정복한다.

  • Filter Chain 메커니즘 (15주차 Filter의 진짜 활용)
  • 인증(Authentication) vs 인가(Authorization)
  • Session 기반 vs Token 기반 (대규모 시스템의 결정)
  • OAuth2와 JWT (현대 표준)

4년차 풀스택 개발자가 가장 자주 마주하지만 "제대로는 모르는" 영역.


🤔 왜 18주차에 Spring Security인가

1~17주차의 빈 공간:

영역주차상태
Java/Spring/JPA1-12주차
DB13-14주차
Spring MVC15주차
분산 시스템16-17주차
Spring Security + 인증/인가미등장

왜 결정적인가:

  1. 면접 거의 100% 출제 — "JWT 인증을 구현해 보셨나요?" "Session vs Token 차이는?"
  2. 15주차 Filter의 진짜 무대 — Spring Security가 Filter 기반의 가장 강력한 사례
  3. 17주차 MSA의 필수 요소 — 분산 시스템에서 인증/인가는 핵심 결정
  4. 실무 95% 사용 — 거의 모든 Spring Boot 앱에 Security 사용
  5. 취약점 직결 — 보안 사고는 커리어와 회사에 치명적

ILIC 관점:

  • 현재 Spring Security 사용 추정 (B2B SaaS는 보안 필수)
  • 그러나 4년차 개발자가 "내부 동작까지 정확히 아는가" 는 또 다른 문제

📊 학습 경로 한눈에 보기

[Phase 1] 인증/인가의 본질 (Authentication vs Authorization)
   ↓
[Phase 2] Spring Security 아키텍처와 Filter Chain ◄ 정점 1
   ↓
[Phase 3] 인증 처리 흐름 (UserDetails, AuthenticationProvider)
   ↓
[Phase 4] 인가 처리 (URL 패턴, 메서드 보안, AOP)
   ↓
[Phase 5] Session 기반 vs Token 기반 ◄ 정점 2 (★ 면접 단골)
   ↓
[Phase 6] JWT 완전 정복 (구조, 검증, 보안 취약점)
   ↓
[Phase 7] OAuth2와 OpenID Connect
   ↓
[Phase 8] 보안 취약점과 방어 (CSRF, XSS, CORS)

총 8 Phase × 27 Unit — 면접 단골 정점 2개를 가진 단일 주차.

🔗 1~18주차 흐름 정리

주차주제의미
1~17주차Java + Spring + JPA + DB + MVC + 분산기능 구현
18주차 (지금)Spring Security + 인증/인가보안 영역

핵심 연결:

  • 15주차 Filter → 18주차에서 본격 활용
  • 8-9주차 AOP → 18주차 메서드 보안 (@PreAuthorize)
  • 17주차 분산 시스템 → 18주차 JWT/OAuth2

🗓️ 권장 학습 일정 (압축 7일)

DayPhase학습 목표
1일차Phase 1 + 2인증/인가 본질 + Filter Chain (★)
2일차Phase 3인증 흐름 (UserDetails 등)
3일차Phase 4인가 (URL + 메서드)
4일차Phase 5Session vs Token (★ 면접 단골)
5일차Phase 6JWT 깊이
6일차Phase 7OAuth2/OIDC
7일차Phase 8 + 종합CSRF/XSS/CORS + 자기 점검

여유 일정 (10일): Phase 2, 5에 +1일씩. 직접 디버거로 Filter Chain step-through 권장.


📚 Phase 1 — 인증/인가의 본질

목표: 가장 자주 헷갈리는 두 개념을 정확히 분리한다.

Unit 1.1 — Authentication vs Authorization ⭐⭐⭐

선수 지식: 없음 (가장 기초)

핵심 정의

Authentication (인증) — "누구인가?":

"이 요청을 보낸 사람이 본인이 맞는가" 검증

Authorization (인가) — "무엇을 할 수 있는가?":

"인증된 사용자가 이 자원에 접근 가능한가" 검증


비유 — 회사 출입:

  • Authentication: 출입증으로 신원 확인 → "박승제 맞으시네요"
  • Authorization: 그 사람이 이 층/방에 들어갈 수 있는가 → "당신은 임원실 출입 권한이 없습니다"

→ 인증 OK여도 인가는 NO일 수 있음


흐름:

[Request]
   ↓
[Authentication]
   - 누구인가? (ID/PW, JWT, 인증서 등)
   - 실패 → 401 Unauthorized
   ↓ (통과)
[Authorization]
   - 권한이 있는가?
   - 실패 → 403 Forbidden
   ↓ (통과)
[Business Logic]

HTTP 상태 코드 (15주차 복습) ⭐ :

  • 401 Unauthorized: 인증 실패 (당신이 누군지 모름)
  • 403 Forbidden: 인가 실패 (당신이 누군지 알지만 권한 없음)

401과 403의 차이는 면접 단골입니다.


ILIC 시나리오:

Authentication:

  • 로그인 → ID/PW 검증 → 사용자 식별
  • 또는 JWT 토큰 검증

Authorization:

  • 일반 사용자 → 자기 운임 견적만
  • 관리자 → 모든 운임 견적
  • 외부 파트너 → 특정 견적만

자기 점검

  • "인증 없이 인가가 가능한가?" (힌트: 사실상 NO — 익명 접근은 별개)
  • "인증은 됐는데 인가가 안 된 사용자에게 401을 주면?" (힌트: 정보 노출 — 반드시 403)

Unit 1.2 — 인증 방식의 진화

선수 지식: Unit 1.1

역사적 진화 ⭐ :

1. Basic Authentication (가장 단순)

Authorization: Basic dXNlcjpwYXNzd29yZA==  (Base64)

문제:

  • Base64는 인코딩 (암호화 X) → 평문 노출
  • 매 요청마다 ID/PW 전송
  • HTTPS 없이는 위험

현재:

  • 내부 API, 단순 시스템에서만 사용

[Login]
   ↓ ID/PW 검증
[Server]
   ↓ Session 생성 + Session ID
[Browser] ← Cookie: SESSIONID=abc123
   ↓ 이후 요청마다 Cookie 자동 전송
[Server] → Session ID로 사용자 식별

장점: 단순, Spring 기본
단점: 서버 상태 보유 — 분산 환경에서 어려움 (Phase 5에서)


3. Token 기반 (JWT 등)

[Login]
   ↓ ID/PW 검증
[Server]
   ↓ JWT 생성 (서명)
[Browser] ← JWT 저장
   ↓ Authorization: Bearer eyJ...
[Server] → JWT 검증 (서명만 확인, 상태 없음)

장점: Stateless, 분산 환경 적합
단점: 토큰 폐기 어려움, 크기 ↑


4. OAuth2 / OIDC (현대 표준)

[Client] → "Google로 로그인"
   ↓
[Google] (Authorization Server) → 사용자 동의
   ↓
[Client] ← Access Token + Refresh Token
   ↓
[Resource Server (API)] ← Access Token으로 인증

장점:

  • 비밀번호를 자체 저장 X
  • 권한 위임 표준
  • 다양한 Provider 통합

현재 표준:

  • 모놀리식 + B2C 웹 → Session-Cookie도 OK
  • SPA + REST API → JWT
  • 모바일 앱 / 서드파티 통합 → OAuth2
  • MSA → JWT (Stateless 핵심)

ILIC 추정:

  • B2B SaaS + Vue 3 SPA → JWT 가능성 ↑
  • 또는 Session-Cookie로 시작 후 점진 전환

자기 점검

  • 4가지 방식의 결정적 차이는?
  • ILIC가 어떤 방식을 쓰고 있고, 왜 그 선택을 했는가?

📚 Phase 2 — Spring Security 아키텍처와 Filter Chain (★ 정점 1)

목표: 면접 단골 — Spring Security가 어떻게 동작하는지 Filter Chain을 통해 이해한다.

Unit 2.1 — Spring Security 위치와 동작 원리

선수 지식: 15주차 Phase 5 (Filter)

핵심 그림 ⭐ :

[Client]
   ↓ HTTP Request
[Servlet Container (Tomcat)]
   ↓
[Filter Chain] ─────── ★ Spring Security가 여기 ★
   ├── DelegatingFilterProxy
   │   └── FilterChainProxy (Spring Security)
   │       ├── SecurityContextPersistenceFilter
   │       ├── UsernamePasswordAuthenticationFilter
   │       ├── BasicAuthenticationFilter
   │       ├── ExceptionTranslationFilter
   │       └── FilterSecurityInterceptor (인가)
   ↓
[DispatcherServlet] (15주차)
   ↓
[Controller]

핵심 통찰:

"Spring Security는 Servlet Filter 로 동작 → DispatcherServlet 도달 전에 차단/통과 결정"

왜 Filter인가 (15주차 복습):

  • DispatcherServlet 외곽 → 가장 강력한 차단 위치
  • Spring 빈 정보 없이도 동작
  • Servlet 표준이라 다른 프레임워크와도 호환

DelegatingFilterProxy:

  • Servlet Filter (표준)
  • 그러나 실제 처리는 Spring 빈으로 위임
  • → Filter 안에서 Spring DI 사용 가능

FilterChainProxy:

  • Spring Security의 진짜 시작점
  • 여러 SecurityFilterChain을 관리
  • 요청 URL에 따라 적절한 Filter 체인 선택

자기 점검

  • 왜 Spring Security Filter들이 일반 Servlet Filter가 아닌 Spring 빈인가? (힌트: DI, AOP)
  • DispatcherServlet과 Spring Security의 호출 순서는? (힌트: Security가 먼저)

Unit 2.2 — Security Filter Chain 핵심 Filter들 ⭐⭐

선수 지식: Unit 2.1

주요 Filter 순서 (실제로는 더 많지만 핵심만):

1. SecurityContextPersistenceFilter
   ↓ SecurityContext를 Session에서 복원
2. UsernamePasswordAuthenticationFilter
   ↓ /login POST 요청 처리
3. BasicAuthenticationFilter
   ↓ Basic Auth 헤더 처리
4. RememberMeAuthenticationFilter
   ↓ Remember-me 쿠키 처리
5. AnonymousAuthenticationFilter
   ↓ 익명 사용자 처리
6. ExceptionTranslationFilter
   ↓ 인증/인가 예외를 HTTP 응답으로
7. FilterSecurityInterceptor
   ↓ 최종 인가 결정

각 Filter의 역할:

SecurityContextPersistenceFilter

  • 요청 시작: Session → SecurityContext 복원
  • 요청 종료: SecurityContext → Session 저장
  • → 요청 사이에 인증 정보 유지

UsernamePasswordAuthenticationFilter ⭐

  • POST /login 요청 가로챔
  • ID/PW 추출 → AuthenticationManager에 위임
  • 성공 → SecurityContext에 저장 + 리다이렉트
  • 실패 → 로그인 페이지로

ExceptionTranslationFilter

  • 뒤쪽 Filter에서 발생한 예외 처리
  • AuthenticationException → 로그인 페이지 (또는 401)
  • AccessDeniedException → 403 페이지

FilterSecurityInterceptor

  • 가장 마지막 Filter
  • 실제 인가 결정 (hasRole, hasAuthority 등)

JWT 사용 시:

  • UsernamePasswordAuthenticationFilter 대신
  • 커스텀 JwtAuthenticationFilter 추가
  • Session 사용 안 함 (SessionCreationPolicy.STATELESS)
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            // ...
        return http.build();
    }
}

자기 점검

  • 왜 Filter 순서가 중요한가? (힌트: 의존성 — Context 복원 후 인증 시도)
  • ExceptionTranslationFilter가 없으면? (힌트: 보안 예외가 컨트롤러까지 흘러감)

Unit 2.3 — SecurityContext와 SecurityContextHolder

선수 지식: Unit 2.2, 4주차 ThreadLocal

핵심 개념

SecurityContext:

  • 현재 인증 정보를 담는 컨테이너
  • Authentication 객체 보유

SecurityContextHolder:

  • SecurityContext에 접근하는 정적 도우미
  • ThreadLocal 기반 ⭐ (4주차 학습)
// 어디서나 현재 사용자 접근 가능
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();

왜 ThreadLocal?:

  • 각 요청은 별도 스레드
  • 스레드별로 다른 사용자 정보 보관
  • 컨트롤러/서비스 어디서나 접근 가능

4주차 ThreadLocal 함정 재등장 ⚠️ :

  • 비동기 처리 (@Async, CompletableFuture)에서 SecurityContext 손실
  • DelegatingSecurityContextRunnable 등으로 전파 필요

Authentication 인터페이스:

public interface Authentication extends Principal {
    Collection<? extends GrantedAuthority> getAuthorities();  // 권한 목록
    Object getCredentials();  // 비밀번호 (검증 후 보통 null)
    Object getDetails();      // 추가 정보 (IP 등)
    Object getPrincipal();    // 사용자 본체 (UserDetails)
    boolean isAuthenticated();
}

구현체 예:

  • UsernamePasswordAuthenticationToken (form 로그인)
  • JwtAuthenticationToken (JWT)
  • OAuth2AuthenticationToken (OAuth2)

Spring Boot에서 현재 사용자 가져오기 (실용):

방법 1 — SecurityContextHolder 직접:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

방법 2 — @AuthenticationPrincipal (권장) ⭐ :

@GetMapping("/me")
public User getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
    return userService.findByUsername(userDetails.getUsername());
}

방법 3 — Authentication 직접 주입:

@GetMapping("/me")
public User getCurrentUser(Authentication authentication) {
    return userService.findByUsername(authentication.getName());
}

자기 점검

  • ThreadLocal로 현재 사용자를 저장하는 위험은? (힌트: 메모리 누수 — request 종료 시 clear 필수)
  • @Async 메서드에서 SecurityContextHolder가 비어있으면? (힌트: 전파 설정 필요)

📚 Phase 3 — 인증 처리 흐름

목표: Spring Security가 ID/PW를 어떻게 검증하는지, 흐름을 따라간다.

Unit 3.1 — UserDetails와 UserDetailsService

선수 지식: Phase 2

핵심 인터페이스

UserDetails — 사용자 정보:

public interface UserDetails {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

구현 예시:

@Getter
public class CustomUserDetails implements UserDetails {
    private final User user;  // JPA 엔티티
    
    @Override
    public String getUsername() { return user.getEmail(); }
    
    @Override
    public String getPassword() { return user.getPasswordHash(); }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .toList();
    }
    
    @Override
    public boolean isAccountNonLocked() { return !user.isLocked(); }
    // ... 나머지
}

UserDetailsService — 사용자 조회:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

구현 예시:

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String email) {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
        return new CustomUserDetails(user);
    }
}

→ Spring Security가 자동으로 이 구현체를 사용


왜 인터페이스를 분리했나 (5주차 OCP, DI):

  • DB가 아닌 LDAP, 외부 API에서도 가져올 수 있도록
  • 테스트 시 In-Memory 구현 가능

자기 점검

  • 왜 password를 UserDetails에 노출시키나? (힌트: AuthenticationProvider 검증용 — 응답에는 절대 노출 X)
  • 5주차의 어떤 패턴인가? (힌트: Strategy)

Unit 3.2 — AuthenticationManager와 AuthenticationProvider ⭐

선수 지식: Unit 3.1

핵심 흐름

AuthenticationManager — 인증 책임자:

  • 인증 요청 받음
  • 적절한 Provider에 위임
  • 보통 ProviderManager 구현체 사용

AuthenticationProvider — 실제 검증:

  • 특정 인증 방식 처리 (DB, LDAP, OAuth2 등)
  • ID/PW 검증, 권한 부여

전체 인증 흐름 ⭐ :

1. UsernamePasswordAuthenticationFilter
   ↓ 요청에서 ID/PW 추출
   ↓ UsernamePasswordAuthenticationToken 생성 (미인증)
   
2. AuthenticationManager.authenticate(token)
   ↓ ProviderManager가 Provider 목록에서 선택
   
3. DaoAuthenticationProvider (대표 Provider)
   ↓ UserDetailsService.loadUserByUsername() 호출
   ↓ DB에서 UserDetails 가져옴
   ↓ PasswordEncoder.matches(rawPassword, hashedPassword) 검증
   ↓ 성공 → UsernamePasswordAuthenticationToken (인증됨) 반환
   
4. SecurityContextHolder에 저장
   
5. 후속 요청에서 자동 인증

PasswordEncoder ⭐ :

  • 평문 비밀번호 저장 X (보안 사고)
  • 해시(Hash) + Salt 로 저장
  • 검증 시 같은 방식으로 해시 후 비교

대표 구현체:

  • BCryptPasswordEncoder (가장 흔함, 권장) ⭐
  • Argon2PasswordEncoder (더 안전)
  • Pbkdf2PasswordEncoder
  • ❌ MD5, SHA-1 (취약 — 절대 X)
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // strength
}

// 사용
String hash = passwordEncoder.encode("rawPassword");
// → "$2a$12$abc..." 같은 형태

boolean match = passwordEncoder.matches("rawPassword", hash);

BCrypt의 특징:

  • Salt 자동 처리 (해시 안에 포함)
  • 느림 (의도적) — 무차별 대입 공격 방어
  • strength 조정 가능 (높을수록 느림 + 안전)

커스텀 AuthenticationProvider:

@Component
public class CustomAuthProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication auth) {
        // 커스텀 검증 로직 (예: 외부 API 호출)
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

ILIC 적용:

  • 일반 사용자: DaoAuthenticationProvider + BCrypt
  • 외부 파트너: 커스텀 Provider (API Key 검증)
  • 관리자: LDAP 연동 가능

자기 점검

  • BCrypt의 strength가 높을수록 좋은가? (힌트: 보안 ↑ 그러나 응답 시간 ↑ — 균형)
  • 같은 비밀번호를 여러 번 해시해도 다른 결과가 나오는 이유는? (힌트: Salt 자동 추가)

Unit 3.3 — Form 로그인 vs API 로그인

선수 지식: Unit 3.2

Form 로그인 (전통 웹):

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .formLogin(form -> form
            .loginPage("/login")
            .loginProcessingUrl("/login")  // POST 처리 URL
            .defaultSuccessUrl("/home")
            .failureUrl("/login?error")
        );
    return http.build();
}

흐름:
1. GET /login → 로그인 페이지 표시
2. POST /login (ID/PW) → UsernamePasswordAuthenticationFilter 가로챔
3. 성공 → /home 리다이렉트 + Session 생성
4. 실패 → /login?error 리다이렉트


API 로그인 (REST + JWT):

@RestController
@RequiredArgsConstructor
public class AuthController {
    private final AuthenticationManager authManager;
    private final JwtTokenProvider jwtProvider;
    
    @PostMapping("/api/login")
    public LoginResponse login(@RequestBody LoginRequest request) {
        // 1. 인증 시도
        Authentication auth = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getEmail(), request.getPassword()
            )
        );
        
        // 2. JWT 생성
        String accessToken = jwtProvider.createAccessToken(auth);
        String refreshToken = jwtProvider.createRefreshToken(auth);
        
        return new LoginResponse(accessToken, refreshToken);
    }
}

흐름:
1. POST /api/login (JSON ID/PW)
2. AuthenticationManager로 직접 인증
3. JWT 생성 후 응답
4. 클라이언트가 JWT 저장 (LocalStorage / HttpOnly Cookie)
5. 이후 요청에 Authorization: Bearer ...


ILIC 시나리오:

  • Vue 3 SPA → API 로그인 + JWT
  • 관리 페이지 (간단) → Form 로그인 가능
  • 모바일 앱 → API + JWT

자기 점검

  • API 로그인에서 왜 AuthenticationManager를 직접 호출? (힌트: Filter가 아닌 컨트롤러에서)
  • Form 로그인을 SPA에서 쓸 수 있는가? (힌트: 가능하지만 부자연스러움)

📚 Phase 4 — 인가 처리

목표: URL 기반과 메서드 기반 인가의 차이와 활용을 마스터한다.

Unit 4.1 — HttpSecurity로 URL 기반 인가

선수 지식: Phase 3

핵심 패턴

Spring Security 6 (현대):

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .requestMatchers("/api/auth/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
            .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
    return http.build();
}

주요 메서드:

메서드의미
permitAll()누구나 접근
denyAll()모두 거부
authenticated()인증된 사용자
hasRole("ADMIN")특정 역할
hasAuthority("READ_PRODUCT")특정 권한
hasAnyRole("ADMIN", "MANAGER")여러 역할 중 하나
access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")SpEL 복잡 표현

Role vs Authority ⭐ :

RoleAuthority
의미역할 그룹세부 권한
ROLE_ADMIN, ROLE_USERREAD_PRODUCT, DELETE_USER
메서드hasRole("ADMIN")hasAuthority("READ_PRODUCT")
Prefix자동 ROLE_ 추가그대로

핵심:

  • hasRole("ADMIN") ↔ DB에 ROLE_ADMIN 저장
  • hasAuthority("ROLE_ADMIN") ↔ DB에 ROLE_ADMIN 저장 (같은 결과)

ILIC 시나리오:

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/auth/**").permitAll()
    .requestMatchers("/api/fares/**").hasAnyRole("USER", "PARTNER", "ADMIN")
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
    .anyRequest().authenticated()
)

순서가 중요 ⚠️ :

  • 위에서부터 매칭 → 첫 매칭 적용
  • 구체적 패턴 → 일반 패턴 순으로

자기 점검

  • /api/admin/users 요청을 일반 사용자가 했다면 어떤 응답? (힌트: 403)
  • /api/admin/users 요청을 인증 없이 했다면? (힌트: 401)

Unit 4.2 — 메서드 보안 (@PreAuthorize, @PostAuthorize) ⭐

선수 지식: Unit 4.1, 8-9주차 AOP

핵심 개념

메서드 보안:

  • URL 패턴이 아닌 메서드 단위 인가
  • AOP 기반 (8-9주차 학습 활용)

활성화:

@Configuration
@EnableMethodSecurity
public class SecurityConfig { ... }

@PreAuthorize — 메서드 호출 검증:

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) { ... }

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public User getUser(Long userId) { ... }

핵심:

  • SpEL 사용 가능
  • 메서드 인자 참조 (#userId)
  • authentication 변수로 현재 사용자

@PostAuthorize — 메서드 호출 검증:

@PostAuthorize("returnObject.owner == authentication.principal.username")
public Document getDocument(Long id) { ... }

→ 반환된 객체 검사 후 권한 결정


@PreFilter / @PostFilter — 컬렉션 필터링:

@PostFilter("filterObject.owner == authentication.principal.username")
public List<Document> getMyDocuments() {
    return documentRepository.findAll();  // 모두 가져온 후 필터
}

⚠️ 주의: 성능 — DB에서 모두 가져와서 필터링

→ 가능하면 쿼리 단계에서 필터링


메서드 보안의 본질 ⭐ :

  • 8-9주차 AOP 메커니즘
  • @Around advice로 메서드 가로챔
  • 자기 호출 함정 동일 (같은 클래스 내 호출 시 안 됨)

ILIC 활용:

@Service
public class FareService {
    
    @PreAuthorize("hasRole('USER')")
    public List<Fare> findAllByCurrentUser() { ... }
    
    @PreAuthorize("hasRole('ADMIN')")
    public void delete(Long id) { ... }
    
    @PreAuthorize("@fareSecurity.canEdit(#fareId, authentication)")
    public Fare update(Long fareId, FareDto dto) { ... }
}

@Component("fareSecurity")
public class FareSecurity {
    public boolean canEdit(Long fareId, Authentication auth) {
        // 복잡한 권한 로직 (소유자 또는 관리자)
    }
}

자기 점검

  • URL 인가 vs 메서드 인가의 결정적 차이는? (힌트: 어디에 명시 — 글로벌 vs 메서드)
  • 8-9주차의 어떤 함정이 메서드 보안에도 적용되는가? (힌트: 자기 호출, @PostConstruct)

📚 Phase 5 — Session 기반 vs Token 기반 (★ 정점 2 — 면접 단골)

목표: 면접에서 거의 100% 출제되는 비교를 명확히 잡는다.

Unit 5.1 — Session 기반 인증의 동작 원리

선수 지식: Phase 2

핵심 흐름

1. [Login Request] POST /login (id, pw)
        ↓
2. [Server] 검증 → Session 생성
   - Session ID (랜덤 문자열) 생성
   - Server 메모리 또는 Redis에 Session 저장
   - Session 데이터: { user_id: 42, role: "USER" }
        ↓
3. [Response] Set-Cookie: SESSIONID=abc123; HttpOnly; Secure
        ↓
4. [Browser] Cookie 자동 저장
        ↓
5. [Subsequent Request] Cookie: SESSIONID=abc123
        ↓
6. [Server] Session ID로 Session 조회 → 사용자 식별

Session 저장소:

1. 서버 메모리:

  • 빠름
  • 서버 재시작 시 모두 손실
  • 다중 서버에서 공유 X ⚠️

2. Redis (분산) ⭐ :

  • 16-17주차의 자연스러운 활용
  • 다중 서버 공유 가능
  • 영속성
spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379
@EnableRedisHttpSession
@Configuration
public class SessionConfig { }

3. JDBC:

  • DB에 Session 저장
  • 느림, 잘 안 씀

Cookie 보안 속성 ⭐ :

속성의미
HttpOnlyJavaScript에서 접근 X (XSS 방어)
SecureHTTPS에서만 전송
SameSite다른 도메인 요청 시 차단 (CSRF 방어)
Path적용 경로
Max-Age만료 시간

모범 사례:

Set-Cookie: SESSIONID=abc; HttpOnly; Secure; SameSite=Lax; Max-Age=3600

Session의 장점 ⭐ :
1. 즉시 무효화 가능 — 서버에서 삭제하면 끝
2. 민감 정보 서버 보관 — Cookie에는 ID만
3. 단순 — Spring 기본

Session의 단점 ⚠️ :
1. 서버 상태 보유 (Stateful) — 분산 환경 부담
2. 수평 확장 어려움 — Sticky Session 또는 공유 저장소 필요
3. CSRF 공격 위험 (자동 Cookie 전송)
4. 모바일 앱과 부자연스러움

자기 점검

  • 서버 3대에 Session을 공유하는 방법 2가지는? (힌트: Sticky Session, Redis 공유)
  • HttpOnly Cookie와 일반 Cookie의 보안 차이는? (힌트: XSS)

Unit 5.2 — Token 기반 인증 (JWT 등) 동작 원리

선수 지식: Unit 5.1

핵심 흐름

1. [Login Request] POST /api/login (email, pw)
        ↓
2. [Server] 검증 → JWT 생성
   - Header + Payload + Signature
   - Payload: { sub: 42, role: "USER", exp: 1700000000 }
   - Signature: 서버 비밀키로 서명
        ↓
3. [Response] { "accessToken": "eyJhbGc..." }
        ↓
4. [Browser/App] 토큰 저장
   - LocalStorage / SessionStorage / HttpOnly Cookie
        ↓
5. [Subsequent Request] Authorization: Bearer eyJhbGc...
        ↓
6. [Server] JWT 검증 (서명만 확인)
   - 서버 메모리 조회 X (Stateless!)
   - Payload에서 사용자 ID 추출

핵심 차이 — 서버 상태:

Session:

  • 서버: { sessionId123: userInfo } 보관
  • 매 요청마다 서버 조회

JWT:

  • 서버: 아무것도 보관 X
  • JWT 자체에 사용자 정보 + 서명
  • Stateless

JWT의 장점 ⭐ :
1. Stateless — 서버 확장 자유
2. MSA 친화 — 서비스 간 토큰 전달 쉬움
3. 다양한 클라이언트 — 모바일, IoT 등
4. SSO 가능 — 토큰을 여러 서비스에서 인정

JWT의 단점 ⚠️ :
1. 즉시 폐기 어려움 — 만료 전까지 유효
2. 토큰 크기 ↑ (Header에 매번 전송)
3. 보안 설정 어려움 — 저장 위치, 만료 등
4. 민감 정보 노출 — Payload는 Base64 (암호화 X)


즉시 폐기 문제 해결:

1. Short-lived Access Token + Refresh Token:

  • Access Token: 15분 ~ 1시간 (짧게)
  • Refresh Token: 1~30일 (길게)
  • 만료 시 Refresh로 재발급

2. Token Blacklist:

  • 폐기된 토큰을 Redis에 저장
  • 매 요청마다 확인
  • Stateless 의 의미 약화 ⚠️

3. 짧은 만료 시간 (15분 정도) + 비밀번호 변경 시 모든 토큰 무효:

  • 가장 실용적 ⭐

자기 점검

  • "JWT는 진짜 Stateless인가?" (힌트: Blacklist 쓰면 NO)
  • Refresh Token도 JWT여야 하나? (힌트: 보통은 단순 랜덤 문자열 + DB 저장 — 즉시 폐기 가능)

Unit 5.3 — Session vs Token 비교 매트릭스 ⭐⭐ (면접 핵심)

선수 지식: Unit 5.1, 5.2

완전 비교 ⭐ :

측면SessionJWT
상태Stateful (서버 보관)Stateless
저장 위치서버 메모리/Redis클라이언트
확장성Sticky 또는 공유 저장소자유
즉시 폐기✅ 가능❌ 어려움
공격 방어CSRF 위험XSS 위험 (저장 위치 따라)
모바일부자연스러움자연스러움
MSA부적합적합
트래픽DB 조회 ↑검증만
크기Cookie ID만매 요청 토큰 전송

선택 가이드 ⭐ :

시나리오추천
전통 웹 모놀리식, B2CSession (단순)
SPA (React/Vue) + REST APIJWT
모바일 앱JWT
MSA / 분산 시스템JWT
금융 / 즉시 차단 필수Session 또는 짧은 JWT
SSO 필요JWT (또는 OAuth2)

ILIC 시나리오 분석:

  • Vue 3 SPA + REST API + B2BJWT 자연스러움
  • 그러나 결제/계약 정보 → 즉시 폐기 가능성 중요 → 짧은 JWT + Refresh

면접 모의 답변 (3분 답변 준비) ⭐ :

"Session과 JWT의 가장 큰 차이는 상태 보유 여부입니다.

Session은 서버에 인증 정보를 보관해서 즉시 폐기가 가능하지만, 분산 환경에서는 서버 간 공유를 위해 Redis 같은 외부 저장소가 필요합니다.

JWT는 토큰 자체에 사용자 정보가 담겨 서버는 서명만 검증하면 되는 Stateless 방식입니다. 확장성이 좋고 마이크로서비스에 적합하지만, 즉시 폐기가 어려워서 짧은 만료 시간 + Refresh Token 패턴을 보통 사용합니다.

저는 Vue SPA 환경에서는 JWT를, 전통 웹에서는 Session을 권장하지만, 즉시 차단이 중요한 결제 같은 영역은 JWT여도 만료를 매우 짧게 하거나 Token Blacklist를 두는 등 추가 설계가 필요 하다고 생각합니다."

자기 점검

  • 위 답변을 본인 ILIC 사례로 어떻게 보강할 수 있을까?
  • "Session이 더 나은 경우는?" — 5초 안에 답변 가능한가?

📚 Phase 6 — JWT 완전 정복

목표: JWT의 구조부터 보안 취약점까지 깊이 있게 마스터한다.

Unit 6.1 — JWT 구조 (Header.Payload.Signature) ⭐

선수 지식: Phase 5

핵심 구조:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiJ9.signature
└── Header ──┘.└── Payload ──┘.└── Signature ──┘

세 부분이 마침표(.) 로 구분.


1. Header

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

알고리즘 종류:

  • HS256 (HMAC-SHA256) — 대칭키
  • RS256 (RSA-SHA256) — 비대칭키 ⭐
  • ES256 (ECDSA) — 비대칭키, 작은 크기

대칭 vs 비대칭:

대칭 (HS256)비대칭 (RS256)
단일 비밀키공개키 + 비밀키
발급/검증같은 키비밀키 발급, 공개키 검증
분산 환경키 공유 어려움검증자가 공개키만
적합단일 서버MSA, OAuth2

MSA에서는 RS256 권장 ⭐


2. Payload

{
  "sub": "42",            // Subject — 사용자 ID
  "name": "Alice",
  "role": "USER",
  "iat": 1700000000,      // Issued At
  "exp": 1700003600       // Expiration (만료)
}

표준 Claim ⭐ :

Claim의미
iss (Issuer)발급자
sub (Subject)주체 (보통 사용자 ID)
aud (Audience)대상
exp (Expiration)만료 시간 ⭐
iat (Issued At)발급 시간
nbf (Not Before)유효 시작 시간
jti (JWT ID)고유 식별자

Custom Claim:

  • role, email 등 자유롭게 추가 가능

⚠️ 주의 ⭐ :

  • Payload는 Base64URL 인코딩 (암호화 X)
  • 누구나 디코딩 가능 → 민감 정보 절대 X
  • 비밀번호, 신용카드 등 절대 X

3. Signature

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

역할 ⭐ :

  • 위변조 방지
  • 클라이언트가 Payload 변경 시도 → Signature 불일치
  • 서버가 검증 시 즉시 거부

핵심 통찰:

"JWT는 암호화 가 아닌 서명 이다. 누구나 내용을 볼 수 있지만, 변조하면 들킨다."


디코딩 도구:

  • jwt.io ⭐ — 브라우저에서 JWT 분해/검증
  • 디버깅 시 유용

자기 점검

  • JWT의 Payload를 누구나 읽을 수 있다는 것의 의미는? (힌트: 민감 정보 X)
  • HS256과 RS256 중 MSA에 적합한 이유는? (힌트: 키 분배)

Unit 6.2 — Spring에서 JWT 구현

선수 지식: Unit 6.1

라이브러리 — JJWT (가장 인기):

implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

JwtTokenProvider 구현:

@Component
public class JwtTokenProvider {
    private final SecretKey secretKey;
    private final long accessTokenValidity = 1000 * 60 * 60;  // 1시간
    private final long refreshTokenValidity = 1000 * 60 * 60 * 24 * 7;  // 7일
    
    public JwtTokenProvider(@Value("${jwt.secret}") String secret) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }
    
    // 토큰 생성
    public String createAccessToken(Authentication auth) {
        UserDetails userDetails = (UserDetails) auth.getPrincipal();
        String authorities = userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));
        
        return Jwts.builder()
            .subject(userDetails.getUsername())
            .claim("authorities", authorities)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + accessTokenValidity))
            .signWith(secretKey, Jwts.SIG.HS256)
            .compact();
    }
    
    // 토큰 검증 + Authentication 반환
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parser()
            .verifyWith(secretKey)
            .build()
            .parseSignedClaims(token)
            .getPayload();
        
        Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get("authorities").toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .toList();
        
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }
    
    // 토큰 유효성 검사
    public boolean validateToken(String token) {
        try {
            Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
            return true;
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT");
        } catch (JwtException | IllegalArgumentException e) {
            log.error("Invalid JWT");
        }
        return false;
    }
}

JwtAuthenticationFilter (커스텀 Filter):

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtProvider;
    
    @Override
    protected void doFilterInternal(
        HttpServletRequest request, 
        HttpServletResponse response, 
        FilterChain filterChain
    ) throws ServletException, IOException {
        String token = resolveToken(request);
        
        if (token != null && jwtProvider.validateToken(token)) {
            Authentication auth = jwtProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

SecurityConfig 통합:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

핵심:

  • STATELESS — 세션 생성 X
  • csrf().disable() — JWT는 CSRF 위험 낮음
  • 커스텀 Filter를 UsernamePasswordAuthenticationFilter 앞에

자기 점검

  • OncePerRequestFilter를 상속? (힌트: 한 요청당 한 번만 — Filter 중복 호출 방지)
  • JWT를 검증할 때 DB 조회가 필요한가? (힌트: 보통 NO — 서명만)

Unit 6.3 — Refresh Token 전략 ⭐

선수 지식: Unit 6.2

문제:

  • Access Token을 길게 → 즉시 폐기 어려움 + 보안 ↓
  • Access Token을 짧게 → 사용자 매번 로그인 = UX ↓

해결 — Access + Refresh 패턴 ⭐ :

[Login]
   ↓
Access Token (15분) + Refresh Token (7일)
   ↓
Access 만료 시:
   ↓
Refresh Token으로 새 Access Token 발급
   ↓
Refresh 만료 시: 재로그인

구현:

1. 로그인 시 둘 다 발급:

@PostMapping("/api/auth/login")
public LoginResponse login(@RequestBody LoginRequest request) {
    Authentication auth = authManager.authenticate(...);
    
    String accessToken = jwtProvider.createAccessToken(auth);
    String refreshToken = jwtProvider.createRefreshToken(auth);
    
    // Refresh Token을 Redis에 저장 (즉시 폐기 가능)
    refreshTokenRepository.save(
        new RefreshToken(auth.getName(), refreshToken, Duration.ofDays(7))
    );
    
    return new LoginResponse(accessToken, refreshToken);
}

2. Access 만료 시 갱신:

@PostMapping("/api/auth/refresh")
public AccessTokenResponse refresh(@RequestBody RefreshRequest request) {
    String refreshToken = request.getRefreshToken();
    
    if (!jwtProvider.validateToken(refreshToken)) {
        throw new InvalidTokenException();
    }
    
    String username = jwtProvider.getUsername(refreshToken);
    
    // Redis 검증 (탈취된 토큰 방어)
    if (!refreshTokenRepository.existsByUsernameAndToken(username, refreshToken)) {
        throw new InvalidTokenException();
    }
    
    // 새 Access Token 발급
    Authentication auth = jwtProvider.getAuthentication(refreshToken);
    return new AccessTokenResponse(jwtProvider.createAccessToken(auth));
}

Refresh Token Rotation (보안 강화) ⭐ :

  • Refresh 사용 시마다 새 Refresh도 발급
  • 옛 Refresh는 즉시 무효
  • 탈취 감지 가능
// refresh 시
String newRefresh = jwtProvider.createRefreshToken(auth);
refreshTokenRepository.delete(oldRefresh);
refreshTokenRepository.save(newRefresh);

저장 위치 결정 ⚠️ :

LocalStorage:

  • 간단
  • XSS 취약 (JS에서 접근 가능)

HttpOnly Cookie:

  • XSS 안전
  • CSRF 위험 (SameSite로 완화)
  • 권장 ⭐

Memory (변수):

  • 가장 안전
  • 페이지 새로고침 시 손실 → 매번 재로그인

실무 패턴:

  • Access Token → Memory
  • Refresh Token → HttpOnly Cookie

ILIC 권장:

  • Access (15분) + Refresh (7일)
  • Refresh Token Rotation 적용
  • HttpOnly Cookie 저장

자기 점검

  • 왜 Refresh Token도 검증을 DB/Redis에서 하는가? (힌트: 즉시 폐기 가능성)
  • "Access Token이 탈취되면?"의 답변은? (힌트: 만료가 짧아 피해 제한적)

Unit 6.4 — JWT 보안 취약점과 방어 ⚠️ (면접 핵심)

선수 지식: Unit 6.1~6.3

주요 취약점 ⭐ :

1. None Algorithm 공격

  • 공격자가 Header를 "alg": "none" 로 변조
  • 서명 없이 검증 통과 시도

방어:

// JJWT는 기본적으로 none 차단 (자동)
// 그러나 명시 권장
.parser()
.verifyWith(secretKey)
.requireIssuer("ilic")
.build()

2. 알고리즘 혼동 공격 (Algorithm Confusion)

  • RS256으로 발급된 토큰을 HS256으로 검증 시도
  • 공개키를 비밀키로 오인

방어:

  • 검증 시 알고리즘 명시
  • 공개키/비밀키 분리 관리

3. 약한 비밀키

  • "mysecret" 같은 짧은 키
  • 무차별 대입 공격 가능

방어:

  • 256비트(32바이트) 이상 권장
  • 환경변수, Secret Manager 사용
// 안전한 키 생성
SecretKey key = Jwts.SIG.HS256.key().build();
String secretString = Encoders.BASE64.encode(key.getEncoded());

4. JWT 탈취

  • LocalStorage에 저장 시 XSS 위험
  • 네트워크 도청

방어:

  • HttpOnly Cookie + Secure + SameSite
  • HTTPS 필수
  • 짧은 만료 시간

5. 만료된 토큰 사용

  • 서버 시간 동기화 문제
  • Clock Skew

방어:

.parser()
.clockSkewSeconds(60)  // 60초 허용
.build()

6. 민감 정보 Payload 노출

  • 비밀번호, 신용카드 등을 Payload에

방어:

  • Payload에는 식별자만
  • 민감 정보는 서버에서 조회

보안 체크리스트 ⭐ :

  • HS256은 32바이트 이상 키
  • MSA에서는 RS256
  • HTTPS 필수
  • HttpOnly Cookie 또는 Memory 저장
  • Access Token 짧게 (15분~1시간)
  • Refresh Token Rotation
  • 검증 시 알고리즘 명시
  • 만료 시간(exp) 항상 포함
  • Payload에 민감 정보 X
  • 비밀키는 환경변수/Secret Manager

자기 점검

  • "JWT의 가장 큰 보안 위험은?" (힌트: 즉시 폐기 어려움 + 탈취 시 만료 전까지 유효)
  • ILIC에서 JWT 사용 시 검증해야 할 보안 사항을 5가지 나열하라

📚 Phase 7 — OAuth2와 OpenID Connect

목표: 현대 인증의 표준인 OAuth2의 흐름을 이해한다.

Unit 7.1 — OAuth2 등장 배경과 역할

선수 지식: Phase 6

문제:

"사용자가 매번 ID/PW 입력하지 않고, 다른 서비스의 인증을 활용 하고 싶다"

: "Google로 로그인", "GitHub으로 로그인"

전통 방식의 위험:

  • A 서비스가 B 서비스의 ID/PW를 받음 → A가 B를 조작 가능
  • A가 비밀번호를 저장 → 탈취 시 모든 서비스 위험

OAuth2의 해결:

"비밀번호를 공유하지 않고 권한만 위임"

비유:

호텔 발렛 키 — 시동만 걸 수 있고 트렁크는 못 엶.


4가지 역할 ⭐ :

역할의미
Resource Owner사용자 (본인)
Client사용하려는 앱 (예: Spotify)
Authorization Server권한 발급 (예: Google)
Resource Server보호된 자원 (예: Google Drive API)

시나리오 — Spotify에서 Google 로그인:

  1. Resource Owner (사용자): Spotify 앱 사용
  2. Client (Spotify): "Google로 로그인" 버튼
  3. Authorization Server (Google): 사용자 인증 + 동의 화면
  4. Resource Server (Google API): Spotify에 사용자 정보 제공

자기 점검

  • 4가지 역할 중 ILIC가 Authorization Server가 될 수 있는가? (힌트: 가능 — 자체 OAuth2 발급)
  • Resource Server와 Authorization Server를 분리하는 이유는? (힌트: 책임 분리, 확장성)

Unit 7.2 — Authorization Code Grant 흐름 ⭐

선수 지식: Unit 7.1

가장 흔한 OAuth2 흐름 (서버 앱):

1. [User] Spotify에서 "Google로 로그인" 클릭
       ↓
2. [Spotify] 사용자를 Google 로그인 페이지로 리다이렉트
   GET https://accounts.google.com/o/oauth2/auth?
       client_id=spotify_client_id&
       redirect_uri=https://spotify.com/callback&
       response_type=code&
       scope=email profile
       ↓
3. [User] Google에 로그인 + 동의 화면
       ↓
4. [Google] Spotify로 리다이렉트 + Authorization Code
   GET https://spotify.com/callback?code=AUTH_CODE
       ↓
5. [Spotify Server] Code로 Access Token 요청
   POST https://oauth2.googleapis.com/token
   { code, client_id, client_secret, redirect_uri, grant_type }
       ↓
6. [Google] Access Token + (Refresh Token) 응답
       ↓
7. [Spotify] Access Token으로 Google API 호출
   GET https://googleapis.com/userinfo
   Authorization: Bearer ACCESS_TOKEN
       ↓
8. [Google] 사용자 정보 반환
       ↓
9. [Spotify] 사용자 등록/로그인 완료

핵심:

  • Code는 한 번만 사용 가능
  • Code → Token 교환은 서버 간 (client_secret 보호)
  • Token은 클라이언트에 노출 X

다른 Grant Type:

Grant용도
Authorization Code일반 웹 앱
Authorization Code + PKCESPA, 모바일
Client Credentials서버 간 (사용자 X)
Resource Owner Password레거시 (권장 X)
Implicit옛날 방식 (권장 X)

PKCE (Proof Key for Code Exchange):

  • SPA/모바일에서 client_secret 저장 위험
  • → 동적으로 생성한 verifier로 대체
  • 현대 표준 ⭐

자기 점검

  • Authorization Code를 직접 받지 않고 Token으로 교환하는 이유는? (힌트: 보안 — 브라우저 노출 방지)
  • ILIC에서 Google/Naver 로그인을 추가한다면 어떤 Grant? (힌트: Authorization Code)

Unit 7.3 — OpenID Connect (OIDC)

선수 지식: Unit 7.2

문제:

  • OAuth2는 인가(권한 위임) 표준
  • "인증(누구인지)"는 명시되지 않음
  • 각 서비스가 다르게 구현 → 표준화 어려움

OIDC (OpenID Connect):

"OAuth2 위에 인증 레이어를 표준화"

핵심 추가:

  • ID Token (JWT) 발급
  • 표준 사용자 정보 엔드포인트 (/userinfo)
  • 표준 Claim (sub, email, name 등)

ID Token vs Access Token:

ID TokenAccess Token
용도사용자 정보API 호출 권한
형식항상 JWTJWT 또는 불투명 문자열
검증Client가 검증Resource Server가 검증
내용사용자 식별권한
// ID Token Payload 예
{
  "iss": "https://accounts.google.com",
  "sub": "10769150350006150715113082367",
  "email": "alice@example.com",
  "name": "Alice",
  "picture": "https://...",
  "iat": 1700000000,
  "exp": 1700003600
}

Spring Security OAuth2 Client:

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: email,profile
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Login(oauth -> oauth
            .defaultSuccessUrl("/home")
            .userInfoEndpoint(userInfo -> userInfo
                .userService(customOAuth2UserService)  // 사용자 처리 커스텀
            )
        );
    return http.build();
}

→ 자동으로 OAuth2/OIDC 흐름 처리

ILIC 시나리오:

  • "Google 로그인" 추가 → OAuth2 Client
  • "ILIC 계정으로 다른 서비스 로그인" → OAuth2 Server (복잡, 보통 Keycloak 같은 솔루션)

자기 점검

  • OAuth2와 OIDC의 차이를 한 문장으로?
  • ILIC가 자체 OAuth2 Server를 만드는 게 좋은가? (힌트: 보통 NO — Keycloak/Auth0 사용)

📚 Phase 8 — 보안 취약점과 방어

목표: 웹 보안의 3대 위협 — CSRF, XSS, CORS — 를 깊이 이해한다.

Unit 8.1 — CSRF (Cross-Site Request Forgery) ⭐

선수 지식: Phase 5

핵심 개념

CSRF:

"다른 사이트에서 사용자의 인증 정보를 활용해 원치 않는 요청을 보내게 함"

시나리오:

1. 사용자가 Bank.com에 로그인 → Cookie 저장
2. 사용자가 Evil.com 방문
3. Evil.com에 숨겨진 폼:
   <form action="https://bank.com/transfer" method="POST">
     <input name="to" value="hacker">
     <input name="amount" value="1000000">
   </form>
4. 자동 제출 → Bank.com에 요청
5. Bank.com: "사용자가 로그인되어 있네" → 송금 처리!

핵심:

  • Cookie는 도메인 기반 자동 전송
  • 사용자가 모르는 사이 요청 발생

방어 방법 ⭐ :

1. CSRF Token

  • 폼마다 랜덤 토큰 발급
  • 서버가 토큰 검증 → 외부 사이트는 토큰 모름
<form>
  <input type="hidden" name="_csrf" value="abc123">
  <!-- ... -->
</form>

Spring Security 기본 활성화:

http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
Set-Cookie: SESSIONID=abc; SameSite=Lax
의미
Strict같은 사이트만 전송 (가장 안전)
LaxGET 등 안전 메서드만 cross-site (기본)
None모든 cross-site (Secure 필수)

3. JWT (Authorization 헤더)

  • Cookie 자동 전송 X
  • JS에서 명시적으로 헤더 설정
  • → CSRF 위험 자동 회피

JWT 사용 시:

http.csrf(AbstractHttpConfigurer::disable);
// JWT는 CSRF 위험 낮음

언제 CSRF 활성화/비활성화?:

  • Session + Cookie → 활성화 ⭐
  • JWT (Authorization 헤더) → 비활성화 OK
  • API 서버 (Stateless) → 비활성화 OK

자기 점검

  • "JWT를 HttpOnly Cookie에 저장하면 CSRF 위험은?" (힌트: 자동 전송 → 위험 — Token 헤더 방식이 안전)
  • SameSite=Strict의 단점은? (힌트: 외부 링크에서 로그인 안 됨)

Unit 8.2 — XSS (Cross-Site Scripting)

선수 지식: Unit 8.1

핵심 개념

XSS:

"악의적 JavaScript 를 다른 사용자의 브라우저에서 실행"


3가지 유형:

1. Stored XSS (저장형) — 가장 위험

1. 공격자: 게시글에 <script>fetch('/api/cookies?c=' + document.cookie)</script>
2. DB 저장
3. 다른 사용자가 게시글 조회 → 스크립트 실행
4. 사용자의 Cookie를 공격자 서버로 전송

2. Reflected XSS (반사형)

URL: /search?q=<script>...</script>
서버가 검색어를 그대로 페이지에 노출 → 실행

3. DOM-based XSS

  • JS가 URL 등에서 값을 받아 DOM에 삽입
  • 서버 거치지 않음

방어 방법 ⭐ :

1. 출력 이스케이프 (가장 중요)

Thymeleaf (자동 이스케이프):

<p th:text="${userInput}">  <!-- 자동 이스케이프 ✅ -->
<p th:utext="${userInput}"> <!-- 이스케이프 X ⚠️ -->

JSP:

<c:out value="${userInput}"/>  <!-- 자동 이스케이프 -->

React/Vue: 기본 이스케이프 적용 ({userInput} vs dangerouslySetInnerHTML)

2. Input Validation

@PostMapping("/comment")
public Comment create(@Valid @RequestBody CommentRequest request) {
    // <, >, " 등 차단 또는 인코딩
}

3. Content Security Policy (CSP)

Content-Security-Policy: default-src 'self'; script-src 'self'

→ 인라인 스크립트 차단

  • JS에서 Cookie 접근 X
  • XSS로 Cookie 탈취 불가
Set-Cookie: SESSIONID=abc; HttpOnly

JWT 저장과 XSS ⚠️ :

  • LocalStorage: XSS 시 직접 탈취 가능
  • HttpOnly Cookie: XSS로도 접근 X (안전)
  • JWT는 HttpOnly Cookie 권장

자기 점검

  • Stored XSS와 Reflected XSS의 차이는?
  • ILIC가 사용자 입력을 받는 모든 곳에서 검증해야 할 사항은?

Unit 8.3 — CORS (Cross-Origin Resource Sharing) ⭐

선수 지식: Phase 1

핵심 개념

Same-Origin Policy (브라우저 보안 기본):

"JS는 같은 origin(scheme + host + port) 의 리소스만 접근 가능"

:

  • https://ilic.com:443 의 JS → https://ilic.com:443/api
  • https://ilic.com:443 의 JS → https://api.ilic.com:443 ❌ (다른 host)
  • https://ilic.com:443 의 JS → http://ilic.com:443 ❌ (다른 scheme)

왜 이 정책?:

  • 악의적 사이트가 본인 사이트의 데이터를 임의로 가져갈 수 없도록

문제 — 현대 웹의 현실:

  • Vue (localhost:3000) ↔ API (localhost:8080) → 다른 origin
  • SPA + REST API 분리 시 항상 발생

CORS — Same-Origin 예외 허용:

"서버가 특정 다른 origin의 요청을 명시적으로 허용"


CORS 흐름 ⭐ :

Simple Request (단순 요청):

  • GET, HEAD, POST
  • 기본 헤더만
  • → Preflight 없이 직접

Preflight Request (사전 요청):

  • PUT, DELETE, 커스텀 헤더 등
  • → OPTIONS 요청 먼저

Preflight 흐름:

1. [Browser] OPTIONS /api/users
   Origin: https://ilic.com
   Access-Control-Request-Method: PUT
   Access-Control-Request-Headers: Authorization
        ↓
2. [Server] 200 OK
   Access-Control-Allow-Origin: https://ilic.com
   Access-Control-Allow-Methods: GET, POST, PUT, DELETE
   Access-Control-Allow-Headers: Authorization
   Access-Control-Allow-Credentials: true
        ↓
3. [Browser] PUT /api/users (실제 요청)

Spring Security CORS 설정:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        // ...
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://ilic.com", "https://admin.ilic.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);  // Cookie 전송 허용
    config.setMaxAge(3600L);  // Preflight 캐싱
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

* 와이드카드 vs 명시적 origin ⚠️ :

config.setAllowedOrigins(List.of("*"));     // ❌ 위험
config.setAllowedOrigins(List.of("https://ilic.com"));  // ✅ 안전

allowCredentials=true 일 때 * 사용 불가 (브라우저가 차단).


ILIC 시나리오:

  • 개발: localhost:3000 (Vue) ↔ localhost:8080 (Spring) → CORS 필요
  • 운영: ilic.comapi.ilic.com → CORS 필요
  • 또는 같은 도메인에 reverse proxy (Nginx)로 배치 → CORS 불필요

자기 점검

  • CORS는 클라이언트 보안인가 서버 보안인가? (힌트: 둘 다 — 브라우저가 강제 + 서버가 허용)
  • ILIC가 CORS를 안 쓰려면? (힌트: 같은 도메인 + 경로로 분리 — /api)

Unit 8.4 — 종합 보안 체크리스트

선수 지식: Unit 8.1~8.3

ILIC 보안 점검 리스트 ⭐ :

인증/인가

  • BCrypt 사용 (MD5/SHA-1 X)
  • 비밀번호 최소 강도 검증
  • 로그인 실패 횟수 제한 (Brute Force 방어)
  • 비밀번호 변경 시 모든 토큰 무효화

JWT

  • 256비트 이상 비밀키
  • HTTPS 필수
  • HttpOnly Cookie 또는 Memory 저장
  • Access Token 짧게 (15분~1시간)
  • Refresh Token Rotation
  • Payload 민감 정보 X
  • 알고리즘 명시 검증

Web 취약점

  • CSRF 보호 (Session 시) 또는 SameSite
  • XSS 방어 (출력 이스케이프, CSP)
  • CORS 명시적 origin (와일드카드 X)
  • HTTPS only
  • 보안 헤더 (X-Frame-Options, X-Content-Type-Options 등)

SQL Injection (13주차)

  • PreparedStatement / JPA 파라미터 바인딩
  • 입력 검증

운영

  • 에러 메시지에 시스템 정보 노출 X
  • 로그에 민감 정보 X
  • 정기 의존성 업데이트 (CVE 추적)
  • 침입 탐지 (모니터링)

자기 점검

  • ILIC의 현재 보안 상태를 위 체크리스트로 평가하라
  • "가장 시급한 보안 개선 3가지"를 선언하라

🎓 종합 자기 점검 (18주차 졸업 시험)

인증/인가 본질

  1. Authentication과 Authorization의 차이를 한 문장씩으로?
  2. 401과 403의 차이는?
  3. 인증 방식 4가지의 진화를 설명하라

Spring Security 아키텍처

  1. Spring Security가 어디에 위치하는지 그림으로?
  2. DelegatingFilterProxy의 역할은?
  3. SecurityFilterChain의 핵심 Filter 5개와 각 역할은?
  4. SecurityContextHolder가 ThreadLocal인 이유는?

인증 처리

  1. UserDetails와 UserDetailsService의 분리 이유는?
  2. AuthenticationManager → AuthenticationProvider 흐름은?
  3. BCrypt가 MD5보다 안전한 2가지 이유는?

인가 처리

  1. URL 인가와 메서드 인가의 차이는?
  2. Role과 Authority의 차이는?
  3. @PreAuthorize와 @PostAuthorize의 차이는?

Session vs Token (★)

  1. Session과 JWT의 결정적 차이를 5가지 비교하라
  2. Stateless의 장단점은?
  3. ILIC는 어떤 방식이 적합하고 왜?

JWT

  1. JWT 3부분(Header, Payload, Signature)의 역할은?
  2. Payload에 민감 정보를 넣으면 안 되는 이유는?
  3. HS256과 RS256의 차이와 MSA에서의 선택은?
  4. Access Token과 Refresh Token의 패턴을 설명하라
  5. JWT 보안 취약점 5가지와 방어는?

OAuth2/OIDC

  1. OAuth2의 4가지 역할은?
  2. Authorization Code Grant 흐름을 설명하라
  3. OIDC가 OAuth2 위에 추가하는 것은?
  4. ID Token과 Access Token의 차이는?

웹 보안

  1. CSRF의 원리와 방어 3가지는?
  2. XSS의 3가지 유형과 방어는?
  3. CORS의 Preflight 흐름은?
  4. JWT를 LocalStorage vs HttpOnly Cookie 어디에?

면접 모의 답변 (실전)

  1. "Spring Security의 동작 원리를 설명해주세요" (5분)
  2. "Session과 JWT 중 어느 것을 선호하시나요? 왜죠?" (3분)
  3. "JWT 구현 시 가장 신경 쓴 보안은?" (3분)
  4. "OAuth2를 직접 구현해 보셨나요?" (2분)

📌 학습 운영 팁

9-섹션 마스터 프롬프트로 깊이 파야 할 Unit

★★★ 면접 단골 (반드시):

  • Unit 1.1 — Authentication vs Authorization
  • Unit 2.2 — SecurityFilterChain
  • Unit 3.2 — AuthenticationManager + BCrypt
  • Unit 5.3 — Session vs JWT 비교 (★ 면접 핵심)
  • Unit 6.1 — JWT 구조
  • Unit 6.4 — JWT 보안 취약점
  • Unit 8.1 — CSRF
  • Unit 8.3 — CORS

★★ 매우 권장:

  • Unit 4.2 — @PreAuthorize
  • Unit 6.3 — Refresh Token 전략
  • Unit 7.2 — Authorization Code Grant
  • Unit 8.4 — 보안 체크리스트

두 정점

Phase 2 (Filter Chain):

  • 15주차 Filter 학습의 진짜 무대
  • 면접에서 "Spring Security 동작 원리?" 질문 100% 출제
  • 직접 디버거로 step-through 하면 영원히 잊지 않음

Phase 5 (Session vs Token):

  • 면접에서 가장 자주 나오는 비교 문제
  • 본인 의견 + ILIC 사례로 답변 가능해야 함

학습 시 주의 — 직접 구현이 핵심

이번 주차는 반드시 작은 프로젝트를 직접 만들어보세요:

  1. Spring Boot + JWT 인증 미니 프로젝트:

    • 회원가입 + 로그인 (BCrypt)
    • JWT 발급/검증 Filter
    • Access + Refresh Token
    • @PreAuthorize 메서드 보안
  2. OAuth2 통합:

    • Google 로그인 추가
    • 사용자 자동 등록
  3. 보안 취약점 실습:

    • CSRF 공격 시뮬레이션 (CSRF 끈 상태)
    • XSS 공격 시뮬레이션
    • CORS 에러 재현 + 해결

이 3가지를 거치면 면접 답변이 자연스러워집니다.

1~18주차 학습 여정

이제 마무리 단계로 가고 있습니다:

영역주차깊이
Java/Spring/JPA1-12★★★
DB13-14★★★
Spring MVC15★★★
분산 시스템16-17★★★
Spring Security18★★★

profile
Software Developer

0개의 댓글