티스토리 뷰

728x90

1. 스프링 시큐리티의 기본 동작 (Session 기반 인증)

스프링 시큐리티(Spring Security)는 기본적으로 세션 기반 인증을 사용합니다.

인증 흐름:

  1. 사용자가 로그인 요청:
    • /login 경로로 POST 요청을 보냅니다.
    • 요청 데이터: usernamepassword (form-data 형식)
  2. UsernamePasswordAuthenticationFilter 작동:
    • 사용자의 usernamepassword를 추출합니다.
public class DefaultLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        UsernamePasswordAuthenticationToken authRequest =
                new UsernamePasswordAuthenticationToken(username, password);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
  1. AuthenticationManager의 인증 시도:
    • UserDetailsService에서 사용자를 로드합니다.
    • 비밀번호를 PasswordEncoder로 비교.
@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
    }
}
  1. 인증 성공:
    • SecurityContextHolderAuthentication 저장.
    • 세션 기반: 기본적으로 세션을 생성하여 인증 정보를 유지합니다.
  2. 인증 실패:
    • SimpleUrlAuthenticationFailureHandler를 통해 에러 메시지 반환.
 

2. Stateless 방식의 JWT (JSON Web Token) 사용

JWT 기반 인증은 상태를 유지하지 않는 Stateless 방식으로, 백엔드 서버가 인증 정보를 유지하지 않습니다.

JWT 인증 흐름:

  1. 사용자가 로그인 요청:
    • /login 경로로 JSON 데이터(email, password)를 POST 요청.
  2. CustomJsonUsernamePasswordAuthenticationFilter 작동:
    • JSON 데이터를 파싱하여 email, password 추출.
public class CustomJsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private final ObjectMapper objectMapper;

    public CustomJsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
        super(new AntPathRequestMatcher("/login", "POST"));
        this.objectMapper = objectMapper;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException {
        String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        Map<String, String> credentials = objectMapper.readValue(messageBody, Map.class);

        String email = credentials.get("email");
        String password = credentials.get("password");

        UsernamePasswordAuthenticationToken authRequest =
                new UsernamePasswordAuthenticationToken(email, password);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
  1. AuthenticationManager의 인증 시도:
    • UserDetailsService에서 사용자 정보 로드 및 비밀번호 검증.
  2. 인증 성공:
    • JwtService를 이용해 AccessTokenRefreshToken 생성.
public class JwtService {
    private final String secretKey = "mysecretkey";

    public String createAccessToken(String email) {
        return Jwts.builder()
                .setSubject(email)
                .setExpiration(new Date(System.currentTimeMillis() + 3600000))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
}
  1. 클라이언트 요청:
    • 이후 요청마다 Authorization: Bearer <AccessToken> 헤더에 포함시켜 요청.
  2. 서버의 JWT 검증:
    • JwtAuthenticationFilter가 요청을 가로채 JWT를 검증.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            String username = jwtService.validateTokenAndGetUsername(token);
            if (username != null) {
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken(username, null, List.of()));
            }
        }
        filterChain.doFilter(request, response);
    }
}
 

3. Stateful (기본) vs Stateless (JWT) 비교

              기능                                        Stateful (세션 기반)                                           Stateless (JWT 기반)

인증 방식 세션에 인증 정보 저장 JWT를 클라이언트가 직접 보관
서버 확장성 서버마다 세션 유지 필요 서버가 인증 정보를 유지하지 않음
인증 정보 유지 세션 유지 (상태 있음) Stateless (무상태)
보안 이슈 세션 하이재킹 가능성 토큰 탈취 시 악용 가능
적합한 환경 소규모 웹 애플리케이션 RESTful API, 마이크로서비스
 

✅ 정리

  • Stateful 방식: 전통적인 세션 기반 로그인, 소규모 프로젝트나 웹 기반에 적합.
  • Stateless JWT 방식: API 기반, 마이크로서비스, 확장성을 필요로 하는 애플리케이션에 적합.

스프링 시큐리티는 기본적으로 Stateful 인증을 제공하지만, JWT를 사용하여 Stateless 방식으로 커스텀할 수 있습니다.

728x90