πŸš€ Java Spring ν”„λ ˆμž„μ›Œν¬μ—μ„œμ˜ 인증 μž‘λ™ 원리와 문제 ν•΄κ²°

@leekh8 Β· September 09, 2024 Β· 5 min read

Code N Solve πŸ“˜: Java Spring ν”„λ ˆμž„μ›Œν¬μ—μ„œμ˜ 인증 μž‘λ™ 원리와 문제 ν•΄κ²°

Spring ν”„λ ˆμž„μ›Œν¬λŠ” μžλ°” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ 인증과 μ ‘κ·Ό μ œμ–΄λ₯Ό μœ„ν•œ κ°•λ ₯ν•˜κ³  μœ μ—°ν•œ 도ꡬ이닀.

Spring Securityλ₯Ό μ΄μš©ν•œ 인증 λ©”μ»€λ‹ˆμ¦˜μ˜ λ™μž‘ 원리와 일반적으둜 λ°œμƒν•  수 μžˆλŠ” 일반적인 문제, 그리고 κ·Έ ν•΄κ²° 방법에 λŒ€ν•΄ μ•Œμ•„λ³΄μž.

Spring Security ? πŸ€”

  • Spring SecurityλŠ” Java 기반 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ ν•„μˆ˜μ μœΌλ‘œ κ³ λ €λ˜μ–΄μ•Ό ν•  μš”μ†Œ 쀑 ν•˜λ‚˜μ΄λ‹€.
  • μ˜¬λ°”λ₯Έ μ‚¬μš©μž 식별과 μ ‘κ·Ό μ œμ–΄λ₯Ό 톡해 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 신뒰성을 높이고 λ³΄μ•ˆμ„ κ°•ν™”ν•  수 μžˆλ‹€.
  • 특히 μ‚¬μš©μž 데이터 λ³΄ν˜Έμ™€ κΆŒν•œ 관리 κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” Spring SecurityλŠ” κ°•λ ₯ν•œ λŒ€μ±…μ΄ 될 수 μžˆλ‹€.

Spring Authentication μž‘λ™ 원리

인증 κ³Όμ •

  • Spring μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ λ‚΄μ—μ„œ μ‚¬μš©μžκ°€ μ‹œμŠ€ν…œμ— μ ‘κ·Όν•˜κΈ° μœ„ν•΄μ„œλŠ” 인증 과정을 μš°μ„  거쳐야 ν•œλ‹€.

  • μ‚¬μš©μž 정보 μž…λ ₯

    • ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— 둜그인 μš”μ²­μ„ 보내면, μ‚¬μš©μžλŠ” 이메일과 λΉ„λ°€λ²ˆν˜Έμ™€ 같은 자격 증λͺ…을 μž…λ ₯ν•œλ‹€.
  • 인증 ν”„λ‘œμ„ΈμŠ€ μ‹œμž‘

    • μ„œλ²„μ—μ„œλŠ” μž…λ ₯된 자격 증λͺ…을 λ°”νƒ•μœΌλ‘œμ‚¬μš©μžλ₯Ό μΈμ¦ν•˜λ €κ³  μ‹œλ„ν•œλ‹€.
    • μ΄λ•Œ AuthenticationManagerλ₯Ό 톡해 μ‚¬μš©μžμ˜ 자격 증λͺ…이 μœ νš¨ν•œμ§€ ν™•μΈν•œλ‹€.
    • Spring SecurityλŠ” 이 과정을 톡해 μ‚¬μš©μž 정보λ₯Ό λ°μ΄ν„°λ² μ΄μŠ€λ‚˜ λ‹€λ₯Έ μ €μž₯μ†Œμ—μ„œ μ‘°νšŒν•˜μ—¬ μœ νš¨μ„±μ„ κ²€μ¦ν•œλ‹€.
    • Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
  • κΆŒν•œ λΆ€μ—¬

    • 인증이 μ„±κ³΅ν•˜λ©΄, μ‚¬μš©μžλŠ” λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯된 μ—­ν• (Role)에 따라 μ μ ˆν•œ κΆŒν•œμ„ λΆ€μ—¬λ°›λŠ”λ‹€.
    • 이후 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ‹€λ₯Έ λΆ€λΆ„μ—μ„œ κΆŒν•œμ΄ κ²€μ¦λ˜μ–΄ μ‚¬μš©μžκ°€ νŠΉμ • μž‘μ—…μ„ μˆ˜ν–‰ν•  수 μžˆλŠ”μ§€ κ²°μ •λœλ‹€.
  • SecurityContextHolder

    • 인증이 μ„±κ³΅ν•˜λ©΄, Spring SecurityλŠ” SecurityContextHolderλ₯Ό μ‚¬μš©ν•˜μ—¬ 인증 정보λ₯Ό κ΄€λ¦¬ν•œλ‹€.
      • SecurityContextHolderλŠ” ν˜„μž¬ 인증된 μ‚¬μš©μžμ˜ μ„ΈλΆ€ 정보λ₯Ό μ €μž₯ν•˜λŠ” μ»¨ν…μŠ€νŠΈμ΄λ‹€.
      • SecurityContextHolder.getContext().setAuthentication(authentication);

인증과 μΈκ°€μ˜ 차이 [^1]? πŸ€”

  • 인증(Identification): 'λˆ„κ΅¬μΈμ§€'λ₯Ό μ•„λŠ” ν”„λ‘œμ„ΈμŠ€λ₯Ό 의미

  • 인가(Authorization): '무엇을 ν•  수 μžˆλŠ”μ§€'λ₯Ό κ²°μ •

  • 두 μš©μ–΄λŠ” μ’…μ’… ν˜Όμš©λ˜μ§€λ§Œ λͺ…ν™•νžˆ ꡬ뢄할 ν•„μš”κ°€ μžˆλ‹€.

  • μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 섀계 μ‹œ 두 κ°œλ…μ„ μ •ν™•νžˆ μ΄ν•΄ν•˜κ³  κ΅¬ν˜„ν•΄μ•Ό ν•œλ‹€.

λ°œμƒ κ°€λŠ₯ν•œ 문제 뢄석 및 였λ₯˜ ν•΄κ²° 방법 [^2]

  • 잘λͺ»λœ μ‚¬μš©μž μž…λ ₯ 처리

    • μ‚¬μš©μžκ°€ μ œκ³΅ν•˜λŠ” μ •λ³΄λŠ” μ’…μ’… λΆ€μ •ν™•ν•  수 μžˆλ‹€.

    • μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 잘λͺ»λœ μž…λ ₯을 κ°μ§€ν•˜κ³ , λͺ…ν™•ν•œ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό μ‚¬μš©μžμ—κ²Œ μ œκ³΅ν•΄μ•Ό ν•œλ‹€.

    • ν•΄κ²° 방법

      • μœ νš¨μ„± 검사(validation)λ₯Ό λ„μž…ν•΄μ•Ό ν•œλ‹€.
      • μœ νš¨μ„± κ²€μ‚¬λŠ” μ‚¬μš©μžκ°€ μ œμΆœν•œ 데이터가 νŠΉμ • 기쀀을 μΆ©μ‘±ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” 과정이닀.
      • Javaμ—μ„œλŠ” javax.validation.constraints νŒ¨ν‚€μ§€λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‰½κ²Œ μœ νš¨μ„± 검사λ₯Ό μ„€μ •ν•  수 μžˆλ‹€.
      • import javax.validation.constraints.Email;
        import javax.validation.constraints.NotBlank;
        
        public class User {
          @NotBlank(message = "이메일은 ν•„μˆ˜ μž…λ ₯ μ‚¬ν•­μž…λ‹ˆλ‹€.")
          @Email(message = "이메일 ν˜•μ‹μ΄ 잘λͺ»λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
          private String email;
        
          @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜ μž…λ ₯ μ‚¬ν•­μž…λ‹ˆλ‹€.")
          private String password;
        }
  • μ„œλ²„ 였λ₯˜ 진단

    • μ„œλ²„μ—μ„œ λ°œμƒν•˜λŠ” 였λ₯˜λŠ” μ‚¬μš©μžκ°€ 직접 ν•΄κ²°ν•˜κΈ° μ–΄λ €μš΄ λ¬Έμ œμ΄λ‹€.
    • 예λ₯Ό λ“€μ–΄, λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ μ„œλ²„λŠ” 더 이상 μš”μ²­μ„ μ²˜λ¦¬ν•  수 μ—†λ‹€.
    • 이런 μƒν™©μ—μ„œλŠ” μ„œλ²„ λ‘œκ·Έμ— 였λ₯˜λ₯Ό κΈ°λ‘ν•˜κ³ , 이λ₯Ό λΉ λ₯΄κ²Œ λΆ„μ„ν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.
    • ν•΄κ²° 방법
      • μ˜ˆμ™Έ 처리λ₯Ό 톡해 μ΄λŸ¬ν•œ 였λ₯˜λ₯Ό ν¬μ°©ν•˜κ³ , 였λ₯˜ λ°œμƒ μ‹œ ν•΄λ‹Ή 였λ₯˜λ₯Ό λ‘œκ·Έμ— 기둝해야 ν•œλ‹€.
      • 이λ₯Ό 톡해 κ°œλ°œμžλŠ” μ„œλ²„μ—μ„œ λ°œμƒν•œ 문제λ₯Ό λΉ λ₯΄κ²Œ μ§„λ‹¨ν•˜κ³  λŒ€μ‘ν•  수 μžˆλ‹€.
      • try {
          // λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ‹œλ„
          connectToDatabase();
        } catch (DatabaseConnectionException e) {
          log.error("λ°μ΄ν„°λ² μ΄μŠ€ 연결에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: {}", e.getMessage());
          throw new ServerException("μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‚˜μ€‘μ— λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.");
        }
  • 인증 μ‹€νŒ¨ 였λ₯˜

    • μ‚¬μš©μžμ˜ 잘λͺ»λœ 자격 증λͺ…, 비정상적인 AuthenticationManager μ„€μ •, λ˜λŠ” μ‚¬μš©μžμ˜ λΉ„λ°€λ²ˆν˜Έκ°€ ν•΄μ‹œλœ ν˜•νƒœλ‘œ μ €μž₯λ˜μ§€ μ•Šκ³  ν‰λ¬ΈμœΌλ‘œ μ €μž₯된 경우 λ“±μœΌλ‘œ λ°œμƒν•˜λŠ” λ¬Έμ œμ΄λ‹€.

    • 인증 μ‹œλ„ ν›„, "Authentication successful" λ‘œκ·Έκ°€ 좜λ ₯λ˜μ§€ μ•Šκ³ , 인증 μ‹€νŒ¨λ‘œ 인해 μ‚¬μš©μžλŠ” λ‘œκ·ΈμΈμ„ ν•  수 μ—†κ²Œ λœλ‹€.

    • ν•΄κ²° 방법

      • μ‚¬μš©μžμ˜ 이메일과 λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄κ²Œ μž…λ ₯λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜κ³ , AuthenticationManager와 UserDetailsServiceκ°€ μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ—ˆλŠ”μ§€ 점검해야 ν•œλ‹€.
      • λ˜ν•œ λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯Έ ν•΄μ‹œ μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•˜μ—¬ μ €μž₯λ˜μ—ˆλŠ”μ§€λ„ 확인해야 ν•œλ‹€.
      • log.info("Login attempt for user: {}", user.getEmail());
        
        try {
          Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
              user.getEmail(), user.getPassword()));
          log.info("Authentication successful for user: {}", user.getEmail());
        } catch (Exception e) {
          log.error("Authentication failed: {}", e.getMessage());
          throw new BadCredentialsException("Invalid credentials");
        }
  • SecurityContextHolder μ„€μ • 문제

    • 인증이 μ„±κ³΅ν–ˆμŒμ—λ„ λΆˆκ΅¬ν•˜κ³  SecurityContextHolder에 인증 정보가 μ €μž₯λ˜μ§€ μ•Šμ•„μ„œ μ΄ν›„μ˜ λ³΄μ•ˆ κ΄€λ ¨ μž‘μ—…μ΄ μ‹€νŒ¨ν•˜λŠ” κ²½μš°κ°€ μžˆλ‹€.

    • 보톡 SecurityContextHolderλ₯Ό μ˜¬λ°”λ₯΄κ²Œ κ΅¬μ„±ν•˜μ§€ μ•Šμ•˜κ±°λ‚˜, 인증 ν›„ 응닡이 λ°˜ν™˜λ˜κΈ° 전에 μ»¨ν…μŠ€νŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ—ˆμ„ λ•Œ λ°œμƒν•©λ‹ˆλ‹€.

    • ν•΄κ²° 방법

      • SecurityContextHolder.getContext().setAuthentication(authentication); 호좜이 μ˜¬λ°”λ₯΄κ²Œ μ΄λ£¨μ–΄μ‘ŒλŠ”μ§€, 그리고 이 호좜 ν›„ μ»¨ν…μŠ€νŠΈκ°€ λ‹€λ₯Έ κ³³μ—μ„œ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜λŠ”μ§€ 확인해야 ν•©λ‹ˆλ‹€.
  • 비동기 μ²˜λ¦¬μ—μ„œμ˜ 인증 문제

    • 비동기 μš”μ²­ 처리 쀑 인증 정보가 μ œλŒ€λ‘œ μ „λ‹¬λ˜μ§€ μ•Šκ±°λ‚˜ μ†μ‹€λ˜λŠ” κ²½μš°κ°€ μžˆλ‹€.

    • 예λ₯Ό λ“€μ–΄, μ‚¬μš©μžκ°€ λ°±κ·ΈλΌμš΄λ“œμ—μ„œ μ‹€ν–‰λ˜λŠ” μž‘μ—…μ„ μš”μ²­ν–ˆμ„ λ•Œ, 인증된 μ‚¬μš©μžμΈμ§€ 확인할 수 μ—†μ–΄ μš”μ²­μ΄ μ‹€νŒ¨ν•  수 μžˆλ‹€.

    • Spring SecurityλŠ” 기본적으둜 μŠ€λ ˆλ“œ 둜컬(ThreadLocal)을 μ‚¬μš©ν•˜μ—¬ SecurityContextλ₯Ό κ΄€λ¦¬ν•œλ‹€.

    • λ”°λΌμ„œ 비동기 ν™˜κ²½μ—μ„œλŠ” 인증 정보가 μŠ€λ ˆλ“œ 간에 μ˜¬λ°”λ₯΄κ²Œ μ „λ‹¬λ˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.

    • ν•΄κ²° 방법

      • 비동기 ν™˜κ²½μ—μ„œ SecurityContextλ₯Ό μœ μ§€ν•˜λ €λ©΄ @Async λ©”μ†Œλ“œ λ˜λŠ” Executorλ₯Ό μ‚¬μš©ν•  λ•Œ SecurityContextλ₯Ό λͺ…μ‹œμ μœΌλ‘œ μ „λ‹¬ν•˜λŠ” λ°©μ‹μœΌλ‘œ μ„€μ •ν•΄μ•Ό ν•œλ‹€.

      • 예λ₯Ό λ“€μ–΄ DelegatingSecurityContextExecutorλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.

      • @Bean
        public Executor taskExecutor() {
          return new DelegatingSecurityContextExecutor(new SimpleAsyncTaskExecutor());
        }

λ³΄μ•ˆ 베슀트 ν”„λž™ν‹°μŠ€ [^3] [^4]

λ³΄μ•ˆμ„±μ„ 높이기 μœ„ν•΄ κ°œλ°œμžλŠ” λͺ¨λ²” 사둀λ₯Ό μ€€μˆ˜ν•΄μ•Ό ν•œλ‹€.

  • ν™•μΈλœ μ•ˆμ „ν•œ 라이브러리 μ‚¬μš©

    • 항상 졜근 λ²„μ „μ˜ 라이브러리λ₯Ό μœ μ§€ν•˜κ³  μ•Œλ €μ§„ 취약점을 주기적으둜 점검해야할 ν•„μš”μ„±μ΄ μžˆλ‹€.
  • HTTP λ³΄μ•ˆ 헀더 μ„€μ •

    • κΈ°λ³Έ HTTP 헀더 μ™Έ μΆ”κ°€ 헀더 값을 톡해 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λ”μš± μ•ˆμ „ν•˜κ²Œ λ³΄ν˜Έν•  수 μžˆλ‹€.
    • 예λ₯Ό λ“€μ–΄, Content-Security-Policy 헀더λ₯Ό μ‚¬μš©ν•˜λ©΄ μ›Ή νŽ˜μ΄μ§€μ—μ„œ 싀행될 수 μžˆλŠ” μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ 좜처λ₯Ό μ œν•œν•  수 μžˆλ‹€.
    • http.headers()
        .contentSecurityPolicy("script-src 'self'")
        .and()
        .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER);
  • 데이터 λͺ…λ Ήμ–΄ 뢄리

    • 데이터λ₯Ό μ²˜λ¦¬ν•˜λŠ” λΆ€λΆ„κ³Ό λͺ…λ Ήμ–΄ μˆ˜ν–‰ 뢀뢄은 μ² μ €νžˆ λΆ„μ‚°ν•˜μ—¬ SQL μ£Όμž… 곡격과 같은 λ³΄μ•ˆ μœ„ν˜‘μ„ μ˜ˆλ°©ν•΄μ•Ό ν•œλ‹€.
    • 예λ₯Ό λ“€μ–΄, SQL 쿼리λ₯Ό 직접 μž‘μ„±ν•˜λŠ” λŒ€μ‹ , PreparedStatementλ₯Ό μ‚¬μš©ν•˜μ—¬ 데이터λ₯Ό μ•ˆμ „ν•˜κ²Œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    •   String query = "SELECT * FROM users WHERE email = ?";
        PreparedStatement statement = connection.prepareStatement(query);
        statement.setString(1, userEmail);
        ResultSet resultSet = statement.executeQuery();

κ²°λ‘ 

μŠ€ν”„λ§(Authentication) 기술이 μ–΄λ–»κ²Œ μž‘λ™ν•˜λ©° μš°λ¦¬κ°€ 마주칠 수 μžˆλŠ” λ¬Έμ œλ“€ 그리고 κ·Έ ν•΄κ²° λ°©μ•ˆμ„ μ‚΄νŽ΄λ³΄μ•˜λ‹€.

각 주제λ₯Ό μ’…ν•©μ μœΌλ‘œ κ³ λ €ν•˜μ—¬ μ•ˆμ •μ μ΄κ³  효율적인 μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ©΄ μ’‹κ² λ‹€.

[^1] Marco Behler - Spring Security: Authentication and Authorization In-Depth (https://www.marcobehler.com/guides/spring-security) [^2] Medium - Securing Spring Boot Applications: Best Practices and ... (https://medium.com/@shubhamvartak01/securing-spring-boot-applications-best-practices-and-strategies-3ab731f8b317) [^3] Spring - Getting Started | Securing a Web Application (https://spring.io/guides/gs/securing-web) [^4] Synopsys - Top 10 Spring Security Best Practices for Java Developers (https://www.synopsys.com/blogs/software-security/spring-security-best-practices.html)

@leekh8
Hello :)