Skip to content

[9 주차] 소성민 / Chapter09_Spring Security - JWT, OAuth - #43

Open
soseongmin03 wants to merge 3 commits into
UMC-AYU:mainfrom
soseongmin03:mino-Chapter09
Open

soseongmin03 wants to merge 3 commits into
UMC-AYU:mainfrom
soseongmin03:mino-Chapter09

Conversation

@soseongmin03

@soseongmin03 soseongmin03 commented May 25, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Number


📝 개요


🚀 주요 변경 사항


🖼️ 실행 결과 (Screenshots)

스크린샷 2026-05-25 140935

💬 고민 및 질문

9주차 2번째 미션이 오류가 많이 발생하여 시간안에 해결이 안될 것 같아 일단 첫번째 미션만 해서 올렸습니다. 기간 이후에라도 완성해 올리도록 하겠습니다.

✅ 실습 체크리스트

  • 이론 학습을 완료 했나요?
  • 미션 요구사항을 이해했나요?
  • 미션을 완료 했나요?

⚙️ 환경 및 컨벤션 체크 (Final Check)

  • 디렉토리 구조 컨벤션을 지켰나요?
  • pr 제목을 컨벤션에 맞게 작성하였나요?
  • pr에 해당되는 이슈를 연결하였나요?
  • Assignees을 본인으로 설정했나요?
  • Reviewers을 설정 했나요?
  • 적절한 라벨을 설정하였나요?

@soseongmin03
soseongmin03 requested review from a team, cho-hj-dev, m4ppy and zldzldzz and removed request for a team May 25, 2026 06:51
Comment on lines +1 to +8
spring:
jpa:
show-sql: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application.yml 에 이미 spring: jpa: 설정이 있는데 따로 yml 파일에서도 중복되게 설정하신 이유가 있을까요?
prod 파일을 따로 만드신 게 환경별 설정 관리를 의도하신 것 같은데 똑같은 속성에 관한 설정이 두 파일에 중복되어 있으니 어떤게 실제로 프로젝트에 적용될 지 잘 모르겠어서 궁금합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 것처럼 환경별 설정 관리를 의도하여 설계한 것입니다. application.yml을 먼저 읽고, spring.profiles.active=prod일 때 application-prod.yml 값을 병합or우선 적용하도록 설계하였습니다.
두 파일에 같은 설정이 중복되어 있기 때문에 충분히 햇갈렸을 수 있다고 생각합니다.

@@ -1,7 +1,8 @@
package org.example.swaggerpr.mission.entity;
package org.example.swaggerpr.store.entity;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스토어와 리전 두 개의 엔티티를 스토어 도메인으로 분리하신 이유가 궁금합니다. 엔티티와 리포지토리만 스토어 도메인에 존재하는데 이것들만 있어도 분리해야 할 이유가 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 상태에서는 “도메인 분리”보다는 “공통 참조 엔티티를 보관할 패키지를 미리 만든 것”에 가깝습니다.
현재 상태로는 이유가 부족하다고 생각하실 수 있지만 파트장님의 피드백을 기반으로 백엔드 관점에서 생각했을 때
mission이나 review에 종속된 데이터라기 보다는 여러 기능에서 참조할 수 있는 엔티티라고 생각해 분리하게 되었습니다.
또 추후에 store와 region에 대한 비즈비스 로직이 확장될 가능성을 열어두기 위해서의 이유도 있습니다.

Comment on lines +56 to +61
UserDetails user = customUserDetailsService.loadUserByUsername(email);
Authentication auth = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT를 사용하는 가장 큰 이유는 서버가 DB를 보지않고도 토큰 서명만 보고 유효성을 검증할 수 있다는 점이라고 생각합니다. 제가 찾아보니까 현재 JWT를 쓰면서도 세션 방식을 사용할 때 처럼 요청마다 DB를 조회하고 있는 것 같습니다.
만약 사용자가 조금 더 많아진다면 DB에 부하가 많이 걸릴 것 같다는 생각이 듭니다.
(혹시 제가 잘못 알고 있는거면 알려주시면 감사하겠습니다..!)

@zldzldzz

zldzldzz commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

로그인 자체가 어려운 부분이라서 다른 분들의 완성된 레포을 읽어보는 것도 많은 도움이 될 것이라고 생각합니다.

권한 정보를 저장하거나 생성하는 코드 자체가 없는 상태입니다.

  • AuthMember.java의 getAuthorities()는 항상 빈 리스트를 반환합니다.
// JwtUtil.java:66은 이 빈 리스트를 문자열로 합치므로 JWT의 role 값은 항상 ""
String authorities = member.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
  • 로그인 여부만 확인하는 authenticated()를 사용하므로 현재 기능은 동작합니다. 하지만 추후 발생할 문제 관리자 API를 다음처럼 추가하면 모든 사용자가 거부됩니다. .requestMatchers("/admin/**").hasRole("ADMIN")와 같이 작성하는 경우 문제가 발생합니다.

// 예시 코드

 public enum MemberRole {                                                                                                                                                                                                          
      USER, ADMIN                                                                                                                                                                                                                   
  }                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                    
  @Enumerated(EnumType.STRING)                                                                                                                                                                                                      
  @Column(nullable = false)                                                                                                                                                                                                         
  private MemberRole role;                                                                                                                                                                                                          
  
  // 회원가입 시 기본값도 지정합니다.                                                                                                                                                                                                                                                                                                                                                                                                                                    
  .role(MemberRole.USER)                                                                                                                                                                                                            
                                                                                                                                                                                                                                    
  //마지막으로 AuthMember에서 Spring Security 권한으로 변환합니다.                                                                                                                                                                                                                                                                                                                                                                                                   
  @Override                                                                                                                                                                                                                         
  public Collection<? extends GrantedAuthority> getAuthorities() {                                                                                                                                                                  
      return List.of(                                                                                                                                                                                                               
          new SimpleGrantedAuthority("ROLE_" + member.getRole().name())                                                                                                                                                             
      );                                                                                                                                                                                                                            
  }       

필터(JwtAuthFilter.java:56)는 매 요청마다 loadUserByUsername으로 DB를 조회해 인증 객체를 만듭니다. 한편 토큰에는 email·role claim을 담아두었는데(JwtUtil.java) 이 claim은 검증에 활용되지 않습니다.

  • 매 요청 DB 조회는 항상 최신 회원 정보를 보장하는 장점이 있지만, 토큰에 담은 claim을 쓰지 않는다면 claim 자체가 불필요한 면이 있습니다. "DB 조회 방식"과 "claim 활용 방식" 중 의도를 명확히 정하면 더 일관됩니다.

토큰 검증에 대한 조건이 부족합니다.

 public boolean isValid(String token) {
        try {
            getClaims(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

// 예시 코드
public boolean isValid(String token) {
    try {
        getClaims(token);
        return true;
    } catch (ExpiredJwtException e) {
        log.info("만료된 JWT 토큰입니다.");
        // 정교한 처리를 위해 여기서 커스텀 예외를 던지거나, 
        // 호출부(Filter 등)에서 이를 인지할 수 있도록 설계 구조를 확장하면 더 좋습니다.
        throw new CustomJwtException(ErrorCode.TOKEN_EXPIRED);
    } catch (SecurityException | MalformedJwtException e) {
        log.error("잘못된 JWT 서명입니다.");
        throw new CustomJwtException(ErrorCode.INVALID_TOKEN);
    } catch (UnsupportedJwtException e) {
        log.error("지원되지 않는 JWT 토큰입니다.");
        throw new CustomJwtException(ErrorCode.UNSUPPORTED_TOKEN);
    } catch (IllegalArgumentException e) {
        log.error("JWT 토큰이 잘못되었습니다.");
        throw new CustomJwtException(ErrorCode.EMPTY_TOKEN);
    }
}

위에 코드로 잡혀 있는 경우 JwtException을 모두 한 번에 잡아 false로 반환합니다. 만료 토큰(ExpiredJwtException)과 서명 위조를 구분하지 못해, 클라이언트가 "토큰 갱신이 필요한지" vs "재로그인이 필요한지"를 알기 어렵습니다. 추후 RefreshToken을 도입한다면 만료를 별도로 잡아 다른 에러코드로 전달하는 것이 적절해 보입니다.

getEmail()의 null 반환 처리

  • JwtUtil.getEmail()은 예외 시 null을 반환합니다. 다만 호출부(JwtAuthFilter.java:51)에서 이미 isValid()로 검증 후 호출하므로 실질적 위험은 낮습니다. 방어적 설계로는 나쁘지 않으나, isValid 통과 후엔 사실상 null이 안 나오므로 중복 방어인 면이 있습니다.
  • 검증과 이메일 추출을 한 번에 처리하고, 이메일이 없으면 명확하게 인증 실패로 처리하는 편이 좋습니다.
 String email = jwtUtil.getEmail(token);                                                                                                                                                                                           
  if (email == null || email.isBlank()) {                                                                                                                                                                                           
      writeUnauthorizedResponse(response);                                                                                                                                                                                          
      return;                                                                                                                                                                                                                       
  }                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                    
// JwtUtil에서 null을 반환하지 않고 예외를 전달합니다.                                                                                                                                                        
// JwtUtil.java                                                                                                                                                                                                                                    
  public String getEmail(String token) {                                                                                                                                                                                            
      String email = getClaims(token).getPayload().getSubject();                                                                                                                                                                    
                                                                                                                                                                                                                                    
      if (email == null || email.isBlank()) {                                                                                                                                                                                       
          throw new JwtException("Token subject is missing.");                                                                                                                                                                      
      }                                                                                                                                                                                                                             
                                                                                                                                                                                                                                    
      return email;                                                                                                                                                                                                                 
  } 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chapter09_Spring Security - JWT, OAuth

4 participants