심화 5 @Builder📚 전체 맵Security FilterChain
05a · 3차 프로젝트 · Spring Security

Security Overview

3차 Spring Security 완전 해설 시리즈 — 개요 · 기술 스택 · 전체 구조 · 학습 로드맵

2차 React+JWT → 3차 Spring Security · 보안 체계화 · 관리자 페이지 · ROLE 기반 권한

Learning Roadmap
이 시리즈의 전체 계획

3차 Spring Security 프로젝트를 5개 파일로 나눠서 완전 해설합니다. 단순히 코드를 나열하는 게 아니라, 왜 이렇게 설계했는지부터 한 줄씩 코드가 어떻게 동작하는지까지 전부 다룹니다. 이 파일(05a)이 첫 번째로, 전체 그림과 기술 스택을 정리합니다.

05a · 현재 파일
개요 & 전체 구조 NOW
  • 이 시리즈 전체 학습 계획 (로드맵)
  • 3차 프로젝트 개요 — 2차에서 뭐가 달라졌나
  • 도입된 기술 스택 전체 + 각 기술이 왜 필요한지
  • 백엔드 / 프론트엔드 파일 구조 전체
  • 신규 파일 / 수정 파일 / 유지 파일 분류
  • 자주 나오는 Java·Spring 용어 사전
05b · 다음 파일
Security & Filter Chain 원리
  • Spring Security란 무엇인가 — 개념부터
  • Filter Chain 동작 원리 상세
  • JWT + Security 결합 흐름
  • UserDetails / UserDetailsService 인터페이스 설계 이유
  • OncePerRequestFilter 개념
  • SecurityContext / SecurityContextHolder
  • Authentication 객체 구조
  • @AuthenticationPrincipal 동작 원리
  • hasRole vs hasAuthority 차이
05c · 백엔드 코드
백엔드 전체 코드 해부
  • entity/User.java — role 필드 + @PrePersist
  • security/CustomUserDetails.java 한 줄씩
  • security/CustomUserDetailsService.java
  • security/JwtAuthenticationFilter.java
  • config/SecurityConfig.java — 모든 설정 항목
  • util/JwtUtil.java — role 추가된 버전
  • service/UserService.java — adminCode 처리
  • controller/BoardController.java
  • controller/AdminController.java
  • service/AdminService.java + repository
05d · 프론트엔드 코드
React 전체 코드 해부
  • App.jsx — PrivateRoute + AdminRoute 설계
  • Login.jsx — role 저장 추가
  • Register.jsx — adminCode 필드
  • BoardList.jsx — 관리자 링크 + 조건부 렌더링
  • AdminPage.jsx — useEffect, 탭 전환, 삭제 처리
  • localStorage 동작 원리
  • React 핵심 문법 총정리
  • axios interceptor 복습
05e · 전체 흐름
요청별 완전 추적 + 1~3차 비교
  • 회원가입 전체 흐름 (React → Spring → DB)
  • 로그인 → JWT 발급 → localStorage 저장
  • 게시글 목록 조회 → 토큰 검증 → SecurityContext
  • 게시글 작성 → @AuthenticationPrincipal 주입
  • 관리자 페이지 접근 → hasRole 검증
  • 관리자 유저 삭제 → 연관 게시글 선삭제
  • 1차 vs 2차 vs 3차 코드 한 줄씩 비교
  • 핵심 변경사항 최종 정리표
What Changed
2차 → 3차, 뭐가 달라졌나

2차에서 JWT 인증은 직접 손으로 만든 JwtFilter로 처리했습니다. 3차는 Spring Security라는 보안 프레임워크 위에 JWT를 얹어서, 인증 체계를 표준화하고 관리자 권한 기능까지 추가합니다.

항목2차 (React + JWT)3차 (Spring Security)
인증 필터 직접 만든 JwtFilter
request.setAttribute로 저장
JwtAuthenticationFilter
SecurityContextHolder에 저장 (표준 방식)
로그인 유저 꺼내기 request.getAttribute("loginUser")
직접 꺼내서 null 체크까지 매번
@AuthenticationPrincipal CustomUserDetails
Spring이 자동 주입 — null 체크 불필요
URL 접근 권한 관리 Controller마다 로그인 체크 코드 반복 SecurityConfig 한 곳에서 전체 URL 권한 설정
CORS 설정 위치 별도 CorsConfig.java에서 설정 SecurityConfig 안의 corsConfigurationSource()로 통합
비밀번호 인코더 등록 BoardApplication.java에서 @Bean 등록 SecurityConfig.java에서 @Bean 등록 (보안 관련은 한 곳으로)
유저 역할(Role) 없음 — 모든 로그인 유저 동일 권한 ROLE_USER / ROLE_ADMIN 구분
관리자 기능 없음 AdminController + AdminService 신규 추가
유저/게시글 강제 삭제
JWT 클레임 username, nickname username, nickname, role 추가
로그인 반환값 token (String) Map<"token", "role"> — role도 함께 반환
📌 핵심 포인트 — 왜 Spring Security를 써야 하나

2차에서 직접 만든 JwtFilter는 동작은 하지만 "Spring Security라는 표준 밖"에 있습니다.
Spring Security를 쓰면:
① URL별 권한 설정을 코드 한 곳(SecurityConfig)에서 관리할 수 있고
@AuthenticationPrincipal로 로그인 유저를 자동 주입받고
hasRole("ADMIN")처럼 역할 기반 권한 제어를 선언적으로 할 수 있습니다.
④ 세션/CSRF/CORS 등 보안 설정을 표준 방식으로 처리합니다.

Tech Stack
도입된 기술 스택 전체

3차에서 새로 추가되거나 중요하게 쓰이는 기술들입니다. 각 기술이 왜 필요한지 함께 이해합니다.

백엔드 — Spring Boot
신규 · 보안 프레임워크
Spring Security
Spring에서 공식 제공하는 보안 전담 프레임워크. Filter Chain으로 모든 요청을 가로채서 인증·권한을 처리한다. 직접 만든 필터보다 훨씬 표준화되고 확장성이 높다.
인터페이스 · Security 표준
UserDetails / UserDetailsService
Spring Security가 유저 정보를 다루는 표준 인터페이스. 우리 User 엔티티를 이 표준에 맞게 포장(CustomUserDetails)해야 Security가 인식한다.
신규 · 인증 객체
SecurityContextHolder
현재 요청의 인증 정보(Authentication)를 담아두는 글로벌 저장소. JwtFilter가 여기에 저장하면 → @AuthenticationPrincipal로 어디서든 꺼낼 수 있다.
기존 유지 · JWT
jjwt (JWT 라이브러리)
2차와 동일. role 클레임을 추가해서 토큰 안에 역할 정보도 담는다. createToken()에 role 파라미터 추가, getRole() 메서드 신규 추가.
기존 유지 · 암호화
BCryptPasswordEncoder
2차와 동일하지만 등록 위치 변경. BoardApplication.java → SecurityConfig.java로 이동 (보안 관련 Bean은 SecurityConfig에서 관리하는 게 원칙).
기존 유지 · DB
Spring Data JPA + MySQL
동일. 단, User 엔티티에 role 필드 추가, BoardRepository에 deleteByUser() 메서드 추가 (유저 삭제 시 게시글 먼저 지워야 하므로).
프론트엔드 — React
수정 · 라우팅 보호
AdminRoute (신규)
기존 PrivateRoute(로그인 여부 체크)에 더해, 역할 기반 라우트 보호 추가. localStorage의 role이 "ROLE_ADMIN"일 때만 관리자 페이지 접근 허용.
신규 · 관리자 페이지
AdminPage.jsx
유저 목록 / 게시글 목록을 탭으로 전환하며 보고, 강제 삭제할 수 있는 관리자 전용 페이지. useEffect로 마운트 시 데이터 로딩.
기존 유지 · HTTP 통신
axios + interceptor
2차와 동일. 모든 요청 헤더에 Bearer 토큰 자동 첨부. 관리자 API(/api/admin/**)도 동일한 interceptor 적용.
기존 유지 · 라우팅
React Router v6
동일. /admin 라우트 추가, AdminRoute로 감싸서 보호. role이 ROLE_ADMIN이 아니면 /board/list로 redirect.
File Structure
전체 파일 구조 — 신규 / 수정 / 유지
백엔드 구조
src/main/java/com/example/board/
├── BoardApplication.java // 동일 (BCryptPasswordEncoder Bean만 제거)
├── config/
│ ├── CorsConfig.java // 역할 축소 (중복 방지용만 남김)
│ └── SecurityConfig.java // ★ 신규 — 전체 보안 설정 핵심 파일
├── controller/
│ ├── UserController.java // 수정 — 로그인 체크 코드 제거
│ ├── BoardController.java // 수정 — @AuthenticationPrincipal 적용
│ └── AdminController.java // ★ 신규 — 관리자 전용 API
├── service/
│ ├── UserService.java // 수정 — role 처리, adminCode 추가, 반환타입 변경
│ ├── BoardService.java // 동일
│ └── AdminService.java // ★ 신규 — 관리자 비즈니스 로직
├── repository/
│ ├── UserRepository.java // 동일
│ └── BoardRepository.java // 수정 — deleteByUser() 추가
├── entity/
│ ├── User.java // 수정 — role 필드 추가, @PrePersist 수정
│ └── Board.java // 동일
├── dto/
│ ├── UserDto.java // 수정 — role, adminCode 필드 추가
│ └── BoardDto.java // 동일
├── security/ // ★ 신규 패키지 전체
│ ├── CustomUserDetails.java // ★ 신규 — UserDetails 구현체
│ ├── CustomUserDetailsService.java // ★ 신규 — UserDetailsService 구현체
│ └── JwtAuthenticationFilter.java // ★ 신규 — 2차 JwtFilter를 Security 표준으로 대체
└── util/
    ├── JwtUtil.java // 수정 — role 클레임 추가, getRole() 추가
    └── ~~JwtFilter.java~~ // 2차에서 사용, 3차에서 제거 (JwtAuthenticationFilter로 대체)
신규 추가 기존 수정 동일 유지
프론트엔드 구조
board-frontend/src/
├── api/
│ └── api.js // 동일 — axios + interceptor (Bearer 토큰 자동 첨부)
├── pages/
│ ├── Login.jsx // 수정 — role localStorage 저장 추가
│ ├── Register.jsx // 수정 — adminCode 입력 필드 추가
│ ├── BoardList.jsx // 수정 — 관리자 링크 추가, 로그아웃 시 role 삭제
│ ├── BoardWrite.jsx // 동일
│ ├── BoardDetail.jsx // 동일
│ ├── BoardEdit.jsx // 동일
│ └── AdminPage.jsx // ★ 신규 — 관리자 유저/게시글 관리 페이지
└── App.jsx // 수정 — AdminRoute 추가, /admin 라우트 등록
신규 추가 기존 수정 동일 유지
Config
build.gradle & application.properties 변경사항
build.gradle — 변경사항
build.gradle
// ===== 2차에서 있었는데 3차에서 제거 =====
// implementation 'org.springframework.security:spring-security-crypto'
// → Spring Security 전체를 추가하면 crypto가 포함되므로 별도 불필요

// ===== 3차에서 새로 추가 =====
implementation "org.springframework.boot:spring-boot-starter-security"
// Spring Security 본체. Filter Chain, SecurityContext, 인증/인가 처리 전체 포함

testImplementation "org.springframework.boot:spring-boot-starter-security"
// 테스트 환경에서도 Security 설정 적용 (없으면 테스트 시 Security 무시됨)

// ===== 기존 유지 =====
implementation "io.jsonwebtoken:jjwt-api:0.12.3"
runtimeOnly   "io.jsonwebtoken:jjwt-impl:0.12.3"
runtimeOnly   "io.jsonwebtoken:jjwt-jackson:0.12.3"
💡 spring-boot-starter-security에 뭐가 들어있나

이 의존성 하나에 다음이 전부 포함됩니다:
spring-security-core — 핵심 인증/인가 기능
spring-security-web — Filter Chain, SecurityContext 등 웹 관련
spring-security-config — @EnableWebSecurity, SecurityFilterChain 설정
spring-security-crypto — BCryptPasswordEncoder (2차에서 별도로 넣었던 것)

추가하는 순간 Spring Boot가 자동으로 모든 URL에 인증 요구를 걸어버립니다. 이를 우리 프로젝트에 맞게 커스텀하는 게 SecurityConfig.java의 역할입니다.

application.properties — 변경사항
application.properties
## ===== 기존 유지 =====
spring.datasource.url=jdbc:mysql://localhost:3306/board
spring.datasource.username=root
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

jwt.secret=mySecretKey2024
jwt.expiration=86400000   // 24시간 (밀리초)

## ===== 3차에서 추가 =====
admin.code=ADMIN2024
// 관리자 코드 — 회원가입 시 이 코드를 입력하면 ROLE_ADMIN 부여
// @Value("${admin.code}")로 UserService에서 읽어옴
// 장점: 코드 수정 없이 properties만 바꾸면 관리자 코드 변경 가능
Glossary
이 시리즈에 자주 나오는 용어 사전

05b~05e를 읽기 전에 이 용어들을 먼저 이해해두면 훨씬 수월합니다.

용어한 줄 설명상세 설명
인터페이스 (interface) "뭘 해야 하는지" 정의만 하는 틀 implements로 구현. Spring Security의 UserDetails, UserDetailsService가 인터페이스 — 우리가 구현체를 만들어야 Security가 쓸 수 있음
@Override 부모/인터페이스 메서드를 구현했다는 표시 없어도 동작하지만 붙이면 컴파일러가 "이게 정말 부모 메서드인지" 검증해줌. 오타 방지 효과
@Bean Spring이 관리할 객체(빈)를 직접 만들어 등록 @Configuration 클래스 안 메서드에 붙임. new BCryptPasswordEncoder()를 Spring 빈으로 등록하면 어디서든 @Autowired/@RequiredArgsConstructor로 주입받을 수 있음
@Component Spring이 자동으로 빈으로 등록 @Service, @Repository, @Controller도 내부적으로 @Component. JwtUtil, JwtAuthenticationFilter에 사용
Filter 요청이 Controller에 도달하기 전에 가로채는 것 Spring Security는 Filter Chain으로 동작. 우리 JwtAuthenticationFilter도 Filter의 일종
Filter Chain 여러 Filter가 순서대로 연결된 파이프라인 요청이 들어오면 Filter Chain의 각 Filter를 순서대로 통과. filterChain.doFilter()가 "다음 Filter로 넘겨라"는 뜻
SecurityContext 현재 요청의 인증 정보 보관함 JwtAuthenticationFilter가 여기에 Authentication 저장 → 이후 @AuthenticationPrincipal로 꺼낼 수 있음. 요청이 끝나면 자동 비워짐
Authentication 인증된 유저 정보를 담는 객체 우리는 UsernamePasswordAuthenticationToken 구현체 사용. (유저 정보, null, 권한 목록)을 담음
@RequiredArgsConstructor final 필드에 대한 생성자 자동 생성 Lombok. private final UserRepository userRepository; 선언만 하면 생성자 주입 자동 처리. @Autowired 안 써도 됨
@Value properties 파일 값을 필드에 주입 @Value("${jwt.secret}") → application.properties의 jwt.secret 값이 필드에 자동 주입됨. 하드코딩 방지
CSRF 세션 기반 인증의 취약점 방어 기법 JWT 사용 시 불필요 (JWT는 매 요청마다 토큰 검증). SecurityConfig에서 .csrf(disable)로 비활성화
STATELESS 서버가 세션을 만들지 않음 JWT 사용하므로 서버 메모리에 세션 저장 불필요. SessionCreationPolicy.STATELESS 설정
GrantedAuthority 권한을 나타내는 인터페이스 SimpleGrantedAuthority("ROLE_USER")가 가장 단순한 구현체. Security가 권한 체크 시 이 목록을 봄
permitAll() 인증 없이 누구나 접근 허용 로그인/회원가입 URL에 적용. 없으면 Security가 미인증 요청을 자동 차단
hasRole("ADMIN") ROLE_ADMIN 권한을 가진 유저만 허용 내부적으로 "ROLE_" 접두사를 자동으로 붙여서 비교. hasAuthority("ROLE_ADMIN")과 결과는 같지만 ROLE_ 자동 추가 여부 차이
05b · Filter Chain
요청 하나가 오면 실제로 뭐가 실행되나

React에서 POST /api/board/write가 오면, Controller의 write() 메서드에 도달하기 전에 아래 Filter들이 순서대로 실행됩니다.

1
SecurityContextHolderFilter Security 기본 제공
SecurityContext(신분증 보관함)를 초기화합니다.
매 요청마다 빈 SecurityContext를 만들어 세팅. 요청이 끝나면 자동으로 비움.
→ JWT는 Stateless — 매 요청마다 새로 인증. 이전 요청 정보가 남아있으면 안 됨.
2
CorsFilter Security 기본 제공
React(localhost:3000)에서 온 요청을 허용합니다.
SecurityConfig의 corsConfigurationSource() 설정을 읽어서 CORS 처리.
허용되지 않은 Origin이면 여기서 차단. Controller까지 가지 않습니다.
3
JwtAuthenticationFilter 우리가 만든 것
JWT 토큰 검증 → SecurityContext에 인증 정보 저장
Authorization: Bearer <token> 헤더 읽기
jwtUtil.validateToken(token) — 유효성 검증
③ 토큰에서 username 추출 → loadUserByUsername() → DB 유저 조회
UsernamePasswordAuthenticationToken 생성 → SecurityContextHolder에 저장
filterChain.doFilter()로 다음 Filter로 넘김
토큰 없거나 유효하지 않으면 SecurityContext에 저장 안 하고 그냥 넘김 (다음 Filter가 처리)
4
AuthorizationFilter Security 기본 제공
SecurityConfig의 설정대로 권한 체크
/api/user/login, /api/user/registerpermitAll() → 누구나 통과
/api/admin/**hasRole("ADMIN") → ADMIN만 통과
• 나머지 → authenticated() → SecurityContext에 Authentication 있어야 통과
통과 못 하면 → 401 Unauthorized 또는 403 Forbidden 자동 반환
5
Controller 도달
모든 Filter를 통과한 요청만 Controller에 도달합니다.
@AuthenticationPrincipal CustomUserDetails userDetails → Spring이 SecurityContext에서 꺼내서 자동 주입.
핵심 개념 카드
SecurityContext
인증 정보 보관함. SecurityContextHolder가 ThreadLocal에 저장. 요청 1개 = 스레드 1개 = SecurityContext 1개. 요청이 끝나면 자동으로 비워짐.
Authentication
SecurityContext 안에 들어가는 인증 객체. UsernamePasswordAuthenticationToken이 구현체. principal(CustomUserDetails), credentials, authorities 포함.
UserDetails / UserDetailsService
Security가 정의한 인터페이스. 왜 직접 구현? Security는 유저 구조를 모름 → 우리 User 엔티티를 Security가 이해하는 형태로 포장하기 위해.
@AuthenticationPrincipal
SecurityContext → Authentication → getPrincipal()을 자동으로 꺼내서 파라미터에 주입. Controller에서 request.getAttribute() 불필요.
hasRole vs hasAuthority
hasRole("ADMIN") → 내부적으로 ROLE_ADMIN을 체크. hasAuthority("ROLE_ADMIN")은 접두사 포함해서 직접 지정. DB에는 ROLE_USER로 저장.
OncePerRequestFilter
JwtAuthenticationFilter의 부모 클래스. 같은 요청에서 Filter가 두 번 실행되는 것을 방지. doFilterInternal()만 오버라이드하면 됨.
05c · 백엔드 코드
핵심 파일별 포인트
CustomUserDetails.java — Security가 이해하는 유저 포장지
CustomUserDetails.java
@Getter
public class CustomUserDetails implements UserDetails {
    private final User user; // 우리 User 엔티티를 그대로 보관

    public CustomUserDetails(User user) { this.user = user; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(user.getRole()));
        // ROLE_USER 또는 ROLE_ADMIN → Security가 권한 체크할 때 사용
    }

    @Override public String getPassword() { return user.getPassword(); }
    @Override public String getUsername() { return user.getUsername(); }
    @Override public boolean isAccountNonExpired()  { return true; }
    @Override public boolean isAccountNonLocked()   { return true; }
    @Override public boolean isCredentialsNonExpired(){ return true; }
    @Override public boolean isEnabled()             { return true; }
}
JwtAuthenticationFilter.java — 핵심 로직
JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                       HttpServletResponse response,
                                       FilterChain filterChain) throws ... {
        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7); // "Bearer " 제거

            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.getUsername(token);
                UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

                // ★ 핵심 — Authentication 객체 만들어서 SecurityContext에 저장
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        filterChain.doFilter(request, response); // 반드시 다음 Filter로 넘겨야 함
    }
}
SecurityConfig.java — 모든 보안 설정의 중심
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())               // JWT 방식 → CSRF 불필요
            .sessionManagement(sm -> sm.sessionCreationPolicy(
                SessionCreationPolicy.STATELESS))      // 세션 안 씀
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/user/login", "/api/user/register").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")  // ROLE_ADMIN만
                .anyRequest().authenticated())              // 나머지는 로그인 필요
            .addFilterBefore(jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
UserService.java — adminCode로 ROLE 결정
UserService.java — register()
public void register(UserDto userDto) {
    String role = ("admin1234".equals(userDto.getAdminCode()))
        ? "ROLE_ADMIN"   // 관리자 코드 일치 → 관리자
        : "ROLE_USER";   // 아니면 일반 유저

    User user = User.builder()
        .username(userDto.getUsername())
        .password(bCryptPasswordEncoder.encode(userDto.getPassword()))
        .nickname(userDto.getNickname())
        .role(role)  // ← 여기서 결정된 role이 DB에 저장됨
        .build();
    userRepository.save(user);
}
BoardController.java — @AuthenticationPrincipal 사용
BoardController.java
@PostMapping("/api/board/write")
public ResponseEntity<?> write(
        @RequestBody BoardDto boardDto,
        @AuthenticationPrincipal CustomUserDetails userDetails) {
    // userDetails → Security가 SecurityContext에서 꺼내서 자동 주입
    // null 체크 불필요 — AuthorizationFilter가 이미 인증 보장
    User user = userDetails.getUser();
    boardService.write(boardDto, user);
    return ResponseEntity.ok("작성 완료");
}
05d · 프론트엔드 코드
React 핵심 — 이중 보안 구조
🔐 3차 프론트엔드 보안 핵심 — 이중 보안

React 라우터 단PrivateRoute(로그인 체크) + AdminRoute(ADMIN 체크)
Spring Security 단hasRole("ADMIN") 서버 권한 체크
프론트에서 막아도 API를 직접 호출하면 뚫릴 수 있음 → 서버에서도 반드시 체크해야 진짜 보안.

App.jsx — PrivateRoute + AdminRoute
App.jsx
// 로그인 여부 체크
const PrivateRoute = ({ children }) => {
    const token = localStorage.getItem("token");
    return token ? children : <Navigate to="/login" />;
};

// 관리자 여부 체크 (3차 신규)
const AdminRoute = ({ children }) => {
    const role = localStorage.getItem("role");
    return role === "ROLE_ADMIN" ? children : <Navigate to="/board/list" />;
};

// 라우터 적용
<Route path="/board/list"  element={<PrivateRoute><BoardList /></PrivateRoute>} />
<Route path="/admin"       element={<AdminRoute><AdminPage /></AdminRoute>} />
Login.jsx — role 저장 추가 (3차 핵심 변경)
Login.jsx — handleLogin()
const response = await api.post("/api/user/login", { username, password });

localStorage.setItem("token", response.data.token);
localStorage.setItem("role", response.data.role); // ← 3차 추가 (ROLE_USER / ROLE_ADMIN)

navigate("/board/list");
BoardList.jsx — 관리자 링크 조건부 렌더링
BoardList.jsx
const role = localStorage.getItem("role");

{role === "ROLE_ADMIN" && (
    <button onClick={() => navigate("/admin")}>관리자 페이지</button>
)}
// role이 ROLE_ADMIN일 때만 버튼 렌더링 — 일반 유저에게는 버튼 자체가 없음
axios interceptor — 토큰 자동 첨부
api.js
const api = axios.create({ baseURL: "http://localhost:8080" });

api.interceptors.request.use(config => {
    const token = localStorage.getItem("token");
    if (token) {
        config.headers["Authorization"] = `Bearer ${token}`;
        // 모든 api.xxx() 호출마다 헤더 자동 첨부 — 개별 컴포넌트에서 토큰 처리 불필요
    }
    return config;
});
05e · 전체 흐름
시나리오별 완전 추적
시나리오 1 — 로그인
1
React · Login.jsx
로그인 버튼 클릭 → POST /api/user/login
axios interceptor: localStorage에 token 없음 → Authorization 헤더 없이 전송
2
JwtAuthenticationFilter
Authorization 헤더 없음 → 건너뜀 → doFilter()로 넘김
SecurityContext에 아무것도 저장 안 함
3
AuthorizationFilter
/api/user/login → permitAll() → 통과
인증 없어도 허용 → Controller로 진행
4
UserService
비밀번호 검증 → JWT 생성 → Map 반환
bCryptPasswordEncoder.matches()jwtUtil.createToken(username, nickname, role)Map.of("token", token, "role", role)
5
React · Login.jsx
token + role → localStorage 저장 → /board/list 이동
localStorage.setItem("token", ...) + localStorage.setItem("role", ...)
시나리오 2 — 게시글 작성 (@AuthenticationPrincipal 핵심)
1
React · BoardWrite.jsx
POST /api/board/write — axios interceptor가 Bearer 토큰 자동 첨부
Authorization: Bearer eyJhbGci...
2
JwtAuthenticationFilter
토큰 검증 → CustomUserDetails 생성 → SecurityContext 저장
loadUserByUsername("hong") → DB 조회 → UsernamePasswordAuthenticationTokenSecurityContextHolder.getContext().setAuthentication(auth)
3
AuthorizationFilter
authenticated() 조건 → SecurityContext에 인증 있음 → 통과
4
BoardController · write()
@AuthenticationPrincipal → User 자동 주입 → boardService.write()
SecurityContext → Authentication → getPrincipal() → CustomUserDetails 자동 주입. null 체크 불필요.
시나리오 3 — 관리자 페이지 접근 (이중 보안)
1
React · AdminRoute
localStorage의 role 확인 → ROLE_ADMIN이어야 AdminPage 렌더링
일반 유저라면 /board/list로 리다이렉트. 버튼 자체도 안 보임.
2
AuthorizationFilter
/api/admin/** → hasRole("ADMIN") → 서버에서도 ROLE_ADMIN 체크
프론트를 우회해서 API 직접 호출해도 여기서 403 차단. 이게 진짜 보안.
3
AdminController → AdminService
유저 삭제 — 게시글 먼저 삭제 → 유저 삭제 (@Transactional)
boardRepository.deleteByUser(user)userRepository.delete(user) — 순서 중요. FK 제약조건 때문에 게시글 먼저.
1차 → 2차 → 3차 전체 비교표
항목1차 (Spring MVC)2차 (React + JWT)3차 (Spring Security)
인증 방식 세션 / 쿠키 JWT (직접 만든 Filter) JWT + Spring Security FilterChain
로그인 유저 꺼내기 session.getAttribute() request.getAttribute("loginUser") @AuthenticationPrincipal 자동 주입
프론트엔드 Thymeleaf (서버 렌더링) React (SPA) React (SPA) + AdminRoute 추가
권한 관리 없음 없음 ROLE_USER / ROLE_ADMIN
URL 권한 설정 Controller 직접 체크 Controller 직접 체크 SecurityConfig 한 곳에서 선언
CORS 불필요 (같은 서버) 별도 CorsConfig.java SecurityConfig 통합
비밀번호 인코더 위치 BoardApplication.java BoardApplication.java SecurityConfig.java (보안 설정 한 곳)
관리자 기능 없음 없음 AdminController + AdminService
Next
다음 파일로 이동
파일핵심 내용선행 이해 필요
06b_security_filterchain.html Spring Security 동작 원리, Filter Chain, JWT+Security 흐름, @AuthenticationPrincipal 이 파일(05a) — 개념 사전 파악 후 읽기
06c_security_backend.html 백엔드 전체 코드 한 줄씩 해부 (Security 파일들 + Controller + Service) 05b — Filter Chain 이해 후 코드 읽기
06d_security_frontend.html React 전체 코드 해부 (AdminRoute, AdminPage, role 처리) 05c — 백엔드 API 이해 후 프론트 읽기
06e_security_full_flow.html 요청별 전체 흐름 완전 추적 + 1~3차 비교표 05b~05d — 코드 다 읽고 나서 흐름 정리용
심화 5 @Builder📚 전체 맵Security FilterChain