Spring Security 사용하기

SIHA·2025년 3월 13일
post-thumbnail

Spring Security란

Spring Security is a framework that provides authentication, authorization, and protection against common attacks.
스프링 기반 인증(Authentication)과 인가(Authorization)를 관리하는 보안 프레임 워크

주요기능

  1. 인증(Authentication) → 사용자가 누구인지 확인 (로그인, JWT, OAuth2)
  2. 권한(Authorization) → 사용자가 특정 기능을 사용할 수 있는지 확인 (Role, Permission)
  3. 보안 필터(Security Filter Chain) → 요청을 가로채 인증 & 인가 처리
  4. CSRF 보호 → 크로스 사이트 요청 위조(CSRF) 공격 방어
  5. CORS 설정 → 교차 출처 리소스 공유(CORS) 정책 관리
  6. 세션 관리 → 로그인 유지(Session) 또는 무상태(Stateless, JWT 기반) 방식 지원

동작 방식

Spring Security는 요청(Request)이 들어오면, SecurityFilterChain에서 필터를 실행하여 인증을 수행하고, 컨트롤러 접근을 결정한다.

[클라이언트] → [Spring Security FilterChain] → [인증(Authentication) 처리] → [권한(Authorization) 체크] → [컨트롤러 실행]

  1. 클라이언트가 요청을 보냄 → Spring Security가 가로챔
  2. JwtAuthenticationFilter 등에서 JWT 검증 후 사용자 인증
  3. 인증된 사용자 정보를 SecurityContextHolder에 저장
  4. 권한(Authorization) 체크를 수행하여 접근 가능 여부 판단
  5. 통과하면 컨트롤러 실행, 실패하면 403 Forbidden 응답 반환

Jwt와 같이 사용하기

본래 Spring Security는 SSR & Session 기반 보안 프레임워크로 Stateful하게 동작하지만, JWT와 함께 사용하려면 Stateless하게 설정해야 한다.

Spring Security 설정하기 (SecurityConfig)

Gradle 의존성 추가

dependencies {
    implementation "org.springframework.boot:spring-boot-starter-security"
}

SecurityConfig

import lombok.RequiredArgsConstructor;
import org.example.statelessspringsecurity.enums.UserRole;
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.config.http.SessionCreationPolicy;
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 JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
                .formLogin(AbstractHttpConfigurer::disable)
                .anonymous(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .rememberMe(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll()
                        .requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
                        .requestMatchers("/open").permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }
}

Spring Security 설정 파일로, 세션과 관련된 Filter들을 비활성화해야한다.

  1. formLogin(AbstractHttpConfigurer::disable)
    : UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
    SSR이 아니기에 폼 기반 로그인 기능이 필요하지 않음
  2. anonymous(AbstractHttpConfigurer::disable)
    : AnonymousAuthenticationFilter 비활성화
    익명 사용자 권한은 필요 없음. (인증은 JWT를 통해 이루어짐)
  3. httpBasic(AbstractHttpConfigurer::disable)
    : BasicAuthenticationFilter 비활성화
    커스텀 Filter를 사용하므로 비활성화
  4. logout(AbstractHttpConfigurer::disable)
    : LogoutFilter 비활성화
    로그아웃은 세션 정보를 지우는 요청이므로, 불필요
  5. rememberMe(AbstractHttpConfigurer::disable)
    : RememberMeAuthenticationFilter 비활성화
    Stateless이기 때문에 Remember를 할 수 없음

+) .anyRequest().authenticated() 의 의미는 SecurityContext에
AbstractAuthenticationToken 이 set이 되어있다면 통과를 시키겠단 의미

JwtAuthenticationFilter

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.statelessspringsecurity.dto.AuthUser;
import org.example.statelessspringsecurity.enums.UserRole;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpRequest,
            @NonNull HttpServletResponse httpResponse,
            @NonNull FilterChain chain
    ) throws ServletException, IOException {
        String authorizationHeader = httpRequest.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String jwt = jwtUtil.substringToken(authorizationHeader);
            try {
                Claims claims = jwtUtil.extractClaims(jwt);

                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    setAuthentication(claims);
                }
            } catch (SecurityException | MalformedJwtException e) {
                log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
            } catch (ExpiredJwtException e) {
                log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
            } catch (UnsupportedJwtException e) {
                log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
            } catch (Exception e) {
                log.error("Internal server error", e);
                httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        chain.doFilter(httpRequest, httpResponse);
    }

    private void setAuthentication(Claims claims) {
        Long userId = Long.valueOf(claims.getSubject());
        String email = claims.get("email", String.class);
        UserRole userRole = UserRole.of(claims.get("userRole", String.class));

        AuthUser authUser = new AuthUser(userId, email, userRole);
        JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}

기존의 Filter 대체

JwtAuthenticationToken

import org.springframework.security.authentication.AbstractAuthenticationToken;

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final AuthUser authUser;

    public JwtAuthenticationToken(AuthUser authUser) {
        super(authUser.getAuthorities());
        this.authUser = authUser;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return authUser;
    }
}
  • Object getPrincipal() 메소드로 컨트롤러 파라미터에서 @AuthenticationPrincipal를 이용해 어떤 인증 객체를 받을 지 설정한다. 즉, 이 상황에서는 우리가 만든 AuthUser를 컨트롤러 메서드에서 리턴받겠다는 것.

  • 이 AuthenticationToken을 SpringSecurityHolder 등록되어 시큐리티 보안을 통과할 수 있게 한다.

profile
뭐라도 해보자

0개의 댓글