1️⃣ 인증과 인가
1. 인증(Authentication) vs 인가(Authorization)
| 구분 | 인증 | 인가 |
| 질문 | 너 누구야! | 권한 있어?! |
| 실패 시 | 401 | 403 |
2. 인증 흐름
클라이언트 요청]
↓
[서버: 인증(Authentication)]
- 로그인 정보 확인
- 토큰 유효성 검사
- 사용자 정보(SecurityContext) 저장
↓
[서버: 인가(Authorization)]
- 요청 URI에 대해 권한 체크
- 접근 가능 여부 판단
↓
[요청 처리 or 차단]
SecurityContext에 사용자 저장
2️⃣ Spring Security
1. 개념
1) Spring Security
- 스프링 기반 애플리케이션의 인증(Authentication)과 인가(Authorization)를 담당하는 보안 프레임워크
- Filter 기반 보안 프레임워크
2) 중요한 점
- JWT 전용 프레임워크 아님
- 세션 로그인, Basic Auth, OAuth2, LDAP 등 다양한 인증 방식 지원(통합 보완 프레임워크)
2. Spring Security 동작
1) 핵심 구조
- 특징: Filter 기반 동작
- 필터들의 묶음: Security Filter Chain
더보기
더보기
- 기본 흐름
[사용자 요청]
↓
[Security Filter Chain]
↓
[AuthenticationManager]
↓
[AuthenticationProvider]
↓
[UserDetailsService]
↓
[User 저장 및 인증 처리]
- 예시) 로그인 요청(세션기반)
1. 로그인 폼에서 username/password 입력
2. `UsernamePasswordAuthenticationFilter`가 요청을 가로챔
3. `AuthenticationManager`에게 인증 위임
4. 내부적으로 `UserDetailsService`에서 사용자 조회
5. 비밀번호 일치 → 인증 성공 → SecurityContext에 저장
6. 인증 실패 → 로그인 페이지로 리다이렉트
3. JWT 인증 방식 도입 시 변경 사항
1) 차이점
| 구분 | 세션 기반 | JWT 기반 |
| 로그인 상태 저장 | 서버 세션 | 클라이언트 토큰 |
| 인증 유지 방식 | 세션 조회 | 토큰 검증 |
| 서버 상태 | 상태 유지(Stateful) | 무상태(Stateless) |
- JWT 기반: 로그인 필터 대신 JwtFilter를 Security Filter Chain에 추가
- Security Filter Chain이 모든 요청을 먼저 검사
2) 흐름
- 인증 성공 시 Authentication 객체가 SecurityContext에 저장
더보기
더보기
- JWT 기반 인증 > 로그인 이후 모든 요청을 커스텀 필터에서 처리
[사용자 요청]
↓
[JwtFilter (커스텀 필터)]
↓
[Security Filter Chain]
↓
[User 저장 및 인증 처리]
- JWT 인증 흐름 요약
1. 사용자가 `/login`에 username/password를 전송
2. 서버는 인증 성공 시 JWT 토큰 생성 → 클라이언트에 반환
3. 이후 모든 요청에 Authorization: Bearer {`JWT`} 헤더 포함
4. 커스텀 필터(`JwtFilter`)가 토큰 유효성 검사
5. 유효한 경우 → Authentication 생성 → SecurityContext에 저장
6. 이후 `인가` 절차로 넘어감
3) Spring Security에 커스텀 JwtFilter 등록 예시
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // .csrf().disable() 방식은 더 이상 사용 안함.
.httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화
.formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
.addFilterBefore(jwtFilter, SecurityContextHolderAwareRequestFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated()
)
.build();
}
- csrf().disable(): REST API 환경에서 CSRF 보호 비활성화 > 세션 기반 로그인
- httpBasic().disable(): 브라우저 기본 인증 방식 비활성화 > 브라우저 기본 로그인 팝업 방식
- formLogin().disable(): 기본 로그인 폼 비활성화 > Spring Security 기본 로그인 페이지
- addFilterBefore(): 커스텀 JwtFilter를 Security 필터 체인에 등록
- authorizeHttpRequests(): URL 기반 인가 정책 설정\
3️⃣ SecurityContext
1. SecurityContextHolder
1) 개념
- 현재 쓰레드에서 인증된 사용자(Authentication 객체)를 저장/조회하는 Spring Security의 핵심 저장소
- 인증 성공 시 Authentication 객체가 SecurityContext에 저장
- 지금 로그인한 사용자가 누구인지를 저장하는 곳
2) 인증 정보 저장 위치
- 코드: 지금 로그인 한 사람 누구임?
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- 커스텀 필터나 컨트롤러, 서비스 어디서든 SecurityContextHolder를 통해 인증 정보에 접근 가능
- 수동 저장(JWT)
SecurityContextHolder.getContext().setAuthentication(authentication);
3) JWT 인증 흐름
- JWT 인증 전체 흐름 핵심 컴포넌트 정리
| 요소 | 역할 |
| JwtFilter | JWT 검사 |
| JwtUtil | 토큰 생성/검증 |
| Authentication | 인증 결과 객체 |
| UsernamePasswordAuthenticationToken | JWT에서 사용하는 인증 객체 |
| SecurityContextHolder | 인증 저장소 |
2. SecurityConfig
1) 코드 + 어노테이션 설명
더보기
더보기
package org.example.nbcam_addvanced_1.common.config;
import lombok.RequiredArgsConstructor;
import org.example.nbcam_addvanced_1.common.filter.JwtFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
private final JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // .csrf().disable() 방식은 더 이상 사용 안함.
.httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화
.formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
.addFilterBefore(jwtFilter, SecurityContextHolderAwareRequestFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/user/login").permitAll()
.anyRequest().authenticated()
)
.build();
}
}
- @Configuration: 설정 클래스
- @EnableWebSecurity: Spring Security를 활성화 / 필터 체인 동작
- @EnableMethodSecurity(securedEnabled = true): @Secured, @PreAuthorize 같은 메서드 단위 권한 설정을 사용
- @RequiredArgsConstructor: final 필드를 생성자로 주입
- private final JwtFilter jwtFilter; : 우리가 만든 JWT 필터를 Security 필터 체인에 등록하기 위해 주입받는 것
2) PasswordEncoder Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- 비밀번호 암호화를 위한 설정: 회원가입 시 비밀번호 암호화 / 로그인 시 암호화 비교
3) securityFilterChain()
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
- 보안 동작 설정
- CSRF 비활성화 / httpBasic 비활성화 / formLogin 비활성화
4) addFilterBefore()
.addFilterBefore(jwtFilter, SecurityContextHolderAwareRequestFilter.class)
- SecurityContext를 사용하는 필터보다 JwtFilter를 먼저 실행
- 흐름
요청
↓
JwtFilter (토큰 검사)
↓
SecurityContext 저장
↓
Spring Security 내부 필터들
↓
Controller
5) URL 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/user/login").permitAll()
.anyRequest().authenticated()
)
- 인가 설정
- /api/user/login 은 누구나 접근 가능
- 나머지는 로그인한 사용자만 접근 가능
- JWT 인증이 안 되어 있으면 접근 불가
- .build(); : 지금까지 설정한 보안 체인을 생성
6) 전체 흐름 정리
1. 요청 도착
2. JwtFilter 실행 (토큰 검사)
3. 유효하면 SecurityContext에 저장
4. 로그인 URL은 누구나 허용
5. 나머지는 인증된 사용자만 허용
6. Controller 실행
7) JwtFilter 클래스
더보기
더보기
package org.example.nbcam_addvanced_1.common.filter;
import static org.example.nbcam_addvanced_1.common.utils.JwtUtil.BEARER_PREFIX;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.nbcam_addvanced_1.common.utils.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// JWT 검증이 필요 없는 경우 ex) 로그인
String requestURI = request.getRequestURI();
if(requestURI.equals("/api/user/login")) {
filterChain.doFilter(request,response);
return;
}
// JWT 토큰이 있는지 없는지 검사
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) {
log.info("JWT 토큰이 필요 합니다.");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰이 필요 합니다.");
return;
}
// JWT 토큰이 있다면 유효한 토큰인지 검사
String jwt = authorizationHeader.substring(7);
if (!jwtUtil.validateToken(jwt)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\": \"Unauthorized\"}");
}
// JWT 토큰에서 복호화 한 데이터 저장하기
String username = jwtUtil.extractUsername(jwt);
request.setAttribute("username", username);
// 이번에 추가된 것
User user = new User(username,"", List.of());
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
filterChain.doFilter(request, response);
}
}
4️⃣ Spring Security 인가 구조
1. pring Security 내부 인가 흐름
- 흐름
SecurityContext에 저장된 Authentication
↓
FilterSecurityInterceptor
↓
AccessDecisionManager
↓
권한 비교 → 허용 or 거부
- 정리
1. SecurityContext에서 현재 사용자 정보 가져옴
2. 요청한 URL이 어떤 권한이 필요한지 확인
3. 사용자 권한과 비교
4. 맞으면 통과
5. 아니면 403 Forbidden
2. 인가 코드 다시보기
- .authenticated() : 인증된 사용자만 허용
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/user/login").permitAll()
.anyRequest().authenticated()
)
3. 인가 설정 방법
1) URL 기반 인가
http.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().permitAll();
- /admin/** → ADMIN만 접근 가능
- /user/** → USER 또는 ADMIN 가능
- 관리하기 쉬움
2) 메서드 기반 인가
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
- - 사용 전 @EnableMethodSecurity(prePostEnabled = true) 필요
4. 인증 인가 실습
1) UserRoleEnum
public enum UserRoleEnum {
ADMIN("ROLE_ADMIN", "관리자 권한"),
NORMAL("ROLE_NORMAL", "일반권한");
private final String role;
private final String description;
}
- 역할: 유저는 어떤 권한을 갖는가?를 코드에서 표준화 / 중앙에서 관리
- role: Spring Security가 이해할 권한 이름
- description: 권한의 설명용
2) 권한 부여: JwtFilter 클래스
// Spring Security가 이해할 수 있는 사용자 객체를 만든다(이름, 권한)
User user = new User(username, "", List.of(userRole::getRole));
SecurityContextHolder.getContext().setAuthentication(
// 사용자가 보낸 요청을 인증된 사용자로 확정해서 등록
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())
// Authentication 객체 만들기 > 인증 완료 상태
// 누가 요청했는지, 비밀번호(JWT 방식이라 null), 권한
);
5. 인가 흐름 정리
'🔌 SPARTA > Courses' 카테고리의 다른 글
| 스탠다드반 9회차: TDD와 단위 테스트 (0) | 2026.03.05 |
|---|---|
| 스탠다드반 8회차 : 로깅(AOP)와 인증/인가의 시작(JWT) (0) | 2026.03.04 |
| JWT와 Filter (0) | 2026.03.03 |
| [숙련 Spring] Spring Data JPA (0) | 2026.02.18 |
| [숙련 Spring] Spring Data JPA (0) | 2026.02.11 |