⚓Spring Security ( 스탠다드 )

2025. 5. 6. 17:36·[내일배움캠프-Sparta]/Spring 6기
반응형

1️⃣ Spring Security 란?

Spring 기반 애플리케이션의 보안을 담당하는 프레임워크

인증, 인가, CSRF, 세션 관리, 비밀번호 암호화 등 다양한 기능 제공

2️⃣ 동작 흐름

기본적으로 Servlet Filter 기반으로 동작하며, FilterChainProxy를 통해 보안 관련 필터들을 순차적으로 적용합니다.

  1. 사용자가 요청(Request)을 보냄
  2. 요청은 FilterChainProxy에 의해 등록된 여러 보안 필터들을 통과
  3. 인증 요청 시 UsernamePasswordAuthenticationFilter가 요청을 가로채고 아이디/비밀번호 추출
  4. 이 정보를 AuthenticationManager에 전달
  5. AuthenticationManager는 하나 이상의 AuthenticationProvider를 통해 인증을 수행
  6. 인증에 성공하면 Authentication 객체를 생성하고 SecurityContextHolder에 저장
  7. 이후 요청 시 이 인증 정보를 통해 권한을 확인하고 접근 허용 여부 결정
  8. 모든 인증 및 인가 절차 후 실제 컨트롤러로 요청 전달됨

💡 인증이 완료되면 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 구조로 세션을 서버에서 저장하지 않기 때문에 수평 확장에 유리함

📣 기본 흐름

  1. 사용자가 아이디/비밀번호로 로그인 요청
  2. 서버는 로그인 정보를 검증하고 JWT를 생성하여 응답으로 반환
  3. 클라이언트는 JWT를 로컬 스토리지나 쿠키 등에 저장
  4. 이후 요청 시 Authorization 헤더에 JWT를 포함시켜 보냄
  5. 서버는 JWT를 필터에서 파싱하고 유효성 검사 후 인증 처리
  6. 인증 정보는 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
'[내일배움캠프-Sparta]/Spring 6기' 카테고리의 다른 글
  • TIL 51 [ JPA 심화 플러스 과제( Lv 1 ), AWS (입문, IAM) ]
  • 테스트 코드 ( 베이직 )
  • 📅 연관관계와 N+1 문제 💥
  • 🔍 영속성과 Transactional
dimenshun
dimenshun
한 소년의 개발 일기
    반응형
  • dimenshun
    Dev Life Notes
    dimenshun
  • 전체
    오늘
    어제
    • 분류 전체보기 (268)
      • CS (23)
        • 자료구조 (0)
        • 알고리즘 (0)
        • 컴퓨터 구조 (8)
        • 네트워크 (6)
        • 운영체제 (3)
        • DB ( + SQLD ) (5)
        • SW공학 (1)
      • 프로그래밍 (3)
        • Java (0)
        • Spring (0)
        • HTML,CSS (3)
        • JavaScript (0)
      • 개발 툴 (7)
        • Git(버전관리) (1)
        • Docker (3)
        • AWS (2)
        • JSP (1)
      • 코딩테스트(Algorithm) (125)
        • 백준 (6)
        • 프로그래머스 (119)
      • [내일배움캠프-Sparta] (110)
        • Spring 6기 (106)
        • KPT 회고 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    docker
    Java
    SQL
    SQLD
    web
    웹
    트랜잭션
    운영체제
    Python
    Testcode
    Til
    메모리
    cs
    개발자
    배포
    spring
    코딩테스트
    컴퓨터구조
    CPU
    KPT
    내일배움캠프
    OS
    세션
    db
    백엔드
    AWS
    네트워크
    network
    It
  • hELLO· Designed By정상우.v4.10.3
dimenshun
⚓Spring Security ( 스탠다드 )
상단으로

티스토리툴바