반응형
1️⃣ Spring Security 란?
Spring 기반 애플리케이션의 보안을 담당하는 프레임워크
인증, 인가, CSRF, 세션 관리, 비밀번호 암호화 등 다양한 기능 제공
2️⃣ 동작 흐름

기본적으로 Servlet Filter 기반으로 동작하며, FilterChainProxy를 통해 보안 관련 필터들을 순차적으로 적용합니다.
- 사용자가 요청(Request)을 보냄
- 요청은 FilterChainProxy에 의해 등록된 여러 보안 필터들을 통과
- 인증 요청 시 UsernamePasswordAuthenticationFilter가 요청을 가로채고 아이디/비밀번호 추출
- 이 정보를 AuthenticationManager에 전달
- AuthenticationManager는 하나 이상의 AuthenticationProvider를 통해 인증을 수행
- 인증에 성공하면 Authentication 객체를 생성하고 SecurityContextHolder에 저장
- 이후 요청 시 이 인증 정보를 통해 권한을 확인하고 접근 허용 여부 결정
- 모든 인증 및 인가 절차 후 실제 컨트롤러로 요청 전달됨
💡 인증이 완료되면 Authentication 객체는 세션 기반으로 저장되며, 이후 요청에서는 해당 객체가 재활용 됨. 만약 stateless한 구조(JWT 등)를 사용할 경우, 이 과정을 별도로 구성해야 합니다.
3️⃣ 주요 컴포넌트
(1) SecurityFilterChain
- 여러 개의 보안 필터들이 연결되어 있는 체인 구조
- 사용자의 요청을 먼저 처리하는 진입 지점
- FilterChainProxy를 통해 등록된 보안 필터가 순서대로 실행됨
- 필터 목록에는 다음과 같은 필터가 존재
- SecurityContextPersistenceFilter: 기존 인증 정보 로드 및 저장
- UsernamePasswordAuthenticationFilter: 폼 로그인 요청 처리
- BasicAuthenticationFilter: HTTP Basic 인증 처리
- ExceptionTranslationFilter: 인증/인가 실패 시 예외 처리
- FilterSecurityInterceptor: 인가(권한) 결정 후 컨트롤러 호출 여부 판단
(2) AuthenticationManager
- 인증 로직을 총괄하는 인터페이스
- 기본 구현체 : ProviderManager
- 다양한 AuthenticationProvider 에게 인증 요청을 위임
(3) AuthenticationProvider
- 실제 인증을 수행하는 컴포넌트
- 예) 사용자 정보를 DB에서 조회하고 비밀번호를 비교하는 로직을 포함
- supports() 메소드를 통해 처리 가능한 Authentication 타입 지정
- 여러 Provider를 등록해 다양한 인증 방식 처리 가능
(4) UserDetailsService ⭐
- 사용자 정보를 로드하는 서비스
- loadUserByUsername(String username) 메소드를 오버라이드하여 구현
- 반환 타입은 UserDetails 객체이며, 사용자 정보 및 권한을 포함
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자 없음"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getAuthorities()
);
}
(5) PasswordEncoder
- 비밀번호를 해싱하거나 비교하는 역할을 수행
- BcryptPasswordEncoder
- 보안을 위해 반드시 평문 비밀번호 비교는 지양, 해시를 비교해야 함.
PasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("password");
boolean matches = encoder.matches("password", encoded); // true
(6) SecurityContextHolder ⭐
- 현재 인증된 사용자에 대한 정보가 저장되는 공간
- SecurityContext 내부에 Authentication 객체가 포함
- 요청 처리 중에는 항상 SecurityContext에서 사용자 정보를 조회 가능
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
// 로그인한 유저의 정보를 가져올수 있음.
4️⃣ 인증 ( Authentication )
- 사용자가 누구인지 확인 하는 과정
- 로그인 요청 처리 과정과 연결
- 인증에 성공하면 Authentication 객체 생성 및 SecurityContextHolder에 저장 ⭐(중요❗❗)
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
}
5️⃣ 인가 ( Authorization )
- 인증된 사용자가 특정 자원에 접근 가능하는지 확인하는 과정
- 스프링에서는 @PreAuthorize, hasRole, hasAuthority 표현식 사용
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {...}
6️⃣ Security 설정 예시 ( SecurityConfig )
@Configuration
@EnableWebSecurity // <--- 이 어노테이션이 들어감
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/login", "/register").permitAll() // WHITE_LIST
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.and()
.logout()
.logoutSuccessUrl("/login");
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
7️⃣ JWT 기반 인증 흐름
📣 개념
- JWT는 서버가 인증 정보를 JSON 형태를 토큰으로 만들어 클라이언트에게 전달하고, 클라이언트는 이후 요청마다 이 토큰을 함께 보내는 방식
- Stareless 구조로 세션을 서버에서 저장하지 않기 때문에 수평 확장에 유리함
📣 기본 흐름
- 사용자가 아이디/비밀번호로 로그인 요청
- 서버는 로그인 정보를 검증하고 JWT를 생성하여 응답으로 반환
- 클라이언트는 JWT를 로컬 스토리지나 쿠키 등에 저장
- 이후 요청 시 Authorization 헤더에 JWT를 포함시켜 보냄
- 서버는 JWT를 필터에서 파싱하고 유효성 검사 후 인증 처리
- 인증 정보는 SecurityContextHolder에 저장되어 컨트롤러에서 사용 가능
📣 JWT 구성
JWT는 .으로 구분된 세 부분으로 구성됩니다.
- Header: 토큰 타입 및 해시 알고리즘 (e.g., HS256)
- Payload: 사용자 정보, 권한 등
- Signature: 위조 여부 확인용 해시 값
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdfQ.YmFzZTY0ZW5jb2RlZHNpZ25hdHVyZQ
📣 필터 구성 ( JwtAuthenticationFilter )
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
String token = bearerToken.substring(7);
if (jwtUtil.validateToken(token)) {
Authentication auth = jwtUtil.getAuthentication(token); ⭐
SecurityContextHolder.getContext().setAuthentication(auth); ⭐
}
}
filterChain.doFilter(request, response);
}
}
📣 JwtUtil ⭐(필수)
public class JwtUtil {
private final String secretKey = "your-secret-key";
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
String username = claims.getSubject();
List<SimpleGrantedAuthority> authorities =
((List<?>) claims.get("roles")).stream()
.map(role -> new SimpleGrantedAuthority((String) role))
.collect(Collectors.toList());
UserDetails userDetails = new User(username, "", authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
}
📣 Spring FilterChain에서 JWT 필터 등록 및 작동 확인
JWT 필터를 SecurityFilterChain에 등록하는 방법은 다음과 같습니다.
일반적으로 UsernamePasswordAuthenticationFilter 이전에 커스텀 필터를 삽입합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/login", "/register", "/todo").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
- addFilterBefore()를 사용하여 Spring Security 내부 필터 순서에 맞춰 삽입
- 이 설정으로 인해 모든 요청은 JWT 필터를 거쳐 인증 처리됨
- 인증 성공 시 SecurityContextHolder에 사용자 정보가 저장되어 이후 인가 처리가 가능함
✅ .permitAll은 SecurityContext 비어 있어도 괜찮다라는 의미이다. (인증)⭐
📣 JWT 토큰 재발급 및 로그아웃 처리 방식
JWT는 기본적으로 만료되면 다시 사용할수 없기 때문에 Refesh Token 개념을 통해 재발급 전략을 구현해야 한다.
1) Access Token + Refresh Token 전략
- Access Token : 짧은 만료 시간 ( 예: 15분 )
- Refresh Token : 긴 만료시간 ( 예: 7일 )
- 클라이언트는 두 개의 토큰을 모두 보관하며, Access Token 만료 시 Refresh Token으로 재발급 요청
2) Refresh Token 저장 위치
- 보안상 서버 저장을 권장 (DB 또는 Redis에 사용자와 매핑 저장)
- 토큰 탈취 시 피해 최소화를 위해 검증 로직 필요
3) 재발급 요청 예시
POST /api/token/refresh
Authorization: Bearer {refresh_token}
4) 로그아웃 처리
- JWT 자체는 상태를 가지지 않기 때문에 "로그아웃" 개념이 존재 ❌
- 로그아웃 시 Refresh Token을 삭제하거나, 블랙리스트(DB/Redis)에 등록
- Access Token은 보통 클라이언트에서 제거
💡 추가적으로 살펴보면 좋은내용
✅ 쿠키 기반 JWT 인증 처리
1️⃣ 쿠키 사용 이유
- 보안 설정 시 HttpOnlyt, Secure 속성을 통해 XSS, CSRF 방어 강화
- 클라이언트에서 Authorization 헤더 대신 쿠키 자동 포함 가능
2️⃣ 서버 응답시 쿠키 설정 예시
ResponseCookie cookie = ResponseCookie.from("access_token", jwtToken)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(Duration.ofMinutes(15))
.build();
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
3️⃣ 쿠키 기반 JWT 인증 처리 흐름
- 클라이언트가 요청 시 쿠키 자동 포함
- 커스텀 필터에서 쿠키를 파싱하고 JWT 검증 후 인증처리
✅ OAuth2 인증 개요 및 적용
Spring Security는 OAuth2.0을 이용한 외부 인증(구글, 네이버, 카카오 등)도 지원함.
1️⃣ 개념
- OAuth2는 제3자 인증 방식으로, 사용자 인증을 외부 서비스(Provider)에서 위임받는 구조입니다.
- 대표적인 Grant Type: Authorization Code, Client Credentials 등
2️⃣ Spring Security OAuth2 설정 예시
spring:
security:
oauth2:
client:
registration:
google:
client-id: [클라이언트 ID]
client-secret: [클라이언트 시크릿]
scope:
- profile
- email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
3️⃣ PrincipalOauth2UserService 구현 예시
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// 사용자 정보 매핑 및 회원가입/로그인 처리
return oAuth2User;
}
}
반응형
'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글
| TIL 51 [ JPA 심화 플러스 과제( Lv 1 ), AWS (입문, IAM) ] (0) | 2025.05.07 |
|---|---|
| 테스트 코드 ( 베이직 ) (0) | 2025.05.06 |
| 📅 연관관계와 N+1 문제 💥 (0) | 2025.05.05 |
| 🔍 영속성과 Transactional (0) | 2025.05.04 |
| TIL 50 [ JPA 심화 - ( 테이블 객체 다루는법, JpaRepository 쿼리,페이징처리, JPQL ,QueryDSL, Auditing, 필요한부분 갱신 ) , 트랜잭션 복습 ] (0) | 2025.05.02 |