diff --git a/src/main/java/com/umc/naoman/domain/member/repository/MemberRepository.java b/src/main/java/com/umc/naoman/domain/member/repository/MemberRepository.java index cb7db885..f399b25c 100644 --- a/src/main/java/com/umc/naoman/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/umc/naoman/domain/member/repository/MemberRepository.java @@ -9,8 +9,6 @@ @Repository public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); - Optional findByAuthIdAndSocialType(String authId, SocialType socialType); - Boolean existsByEmail(String email); + Optional findBySocialTypeAndAuthId(SocialType socialType, String authId); Boolean existsBySocialTypeAndAuthId(SocialType socialType, String authId); } diff --git a/src/main/java/com/umc/naoman/domain/member/service/MemberServiceImpl.java b/src/main/java/com/umc/naoman/domain/member/service/MemberServiceImpl.java index 5af55be7..3f40c188 100644 --- a/src/main/java/com/umc/naoman/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/member/service/MemberServiceImpl.java @@ -57,27 +57,27 @@ public LoginInfo signup(SignupRequest request) { Member member = memberConverter.toEntity(request); memberRepository.save(member); + Long memberId = member.getId(); // 회원가입 완료 후 로그인 처리를 위해 access token, refresh token 발급 // 별도 권한 정책이 없으므로 default 처리 String role = "ROLE_DEFAULT"; - String email = member.getEmail(); - Long memberId = member.getId(); - String accessToken = jwtUtils.createJwt(email, role, ACCESS_TOKEN_VALIDITY_IN_SECONDS); - String refreshToken = jwtUtils.createJwt(email, role, REFRESH_TOKEN_VALIDITY_IN_SECONDS); - refreshTokenService.saveRefreshToken(memberId, refreshToken); - return memberConverter.toLoginInfo(memberId, accessToken, refreshToken); + + return createJwtAndGetLoginInfo(memberId, role); } @Override public LoginInfo login(LoginRequest request) { Member member = findMember(request.getSocialType(), request.getAuthId()); - Long memberId = member.getId(); - String email = member.getEmail(); String role = "ROLE_DEFAULT"; - String accessToken = jwtUtils.createJwt(email, role, ACCESS_TOKEN_VALIDITY_IN_SECONDS); - String refreshToken = jwtUtils.createJwt(email, role, REFRESH_TOKEN_VALIDITY_IN_SECONDS); + + return createJwtAndGetLoginInfo(memberId, role); + } + + private LoginInfo createJwtAndGetLoginInfo(Long memberId, String role) { + String accessToken = jwtUtils.createJwt(memberId, role, ACCESS_TOKEN_VALIDITY_IN_SECONDS); + String refreshToken = jwtUtils.createJwt(memberId, role, REFRESH_TOKEN_VALIDITY_IN_SECONDS); refreshTokenService.saveRefreshToken(memberId, refreshToken); return memberConverter.toLoginInfo(memberId, accessToken, refreshToken); @@ -113,7 +113,7 @@ public Member findMember(Long memberId) { @Override public Member findMember(SocialType socialType, String authId) { - return memberRepository.findByAuthIdAndSocialType(authId, socialType) + return memberRepository.findBySocialTypeAndAuthId(socialType, authId) .orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND_BY_AUTH_ID_AND_SOCIAL_TYPE)); } } diff --git a/src/main/java/com/umc/naoman/domain/photo/elasticsearch/index/sample_photo_vectors.json b/src/main/java/com/umc/naoman/domain/photo/elasticsearch/index/sample_face_vectors.json similarity index 100% rename from src/main/java/com/umc/naoman/domain/photo/elasticsearch/index/sample_photo_vectors.json rename to src/main/java/com/umc/naoman/domain/photo/elasticsearch/index/sample_face_vectors.json diff --git a/src/main/java/com/umc/naoman/domain/photo/elasticsearch/repository/PhotoEsClientRepository.java b/src/main/java/com/umc/naoman/domain/photo/elasticsearch/repository/PhotoEsClientRepository.java index ffc8db77..e28a209c 100644 --- a/src/main/java/com/umc/naoman/domain/photo/elasticsearch/repository/PhotoEsClientRepository.java +++ b/src/main/java/com/umc/naoman/domain/photo/elasticsearch/repository/PhotoEsClientRepository.java @@ -229,10 +229,7 @@ public List deletePhotoEsByFaceTag(Long memberId) { } //특정 공유 그룹의 사진 삭제 -> 해당 사진에서 감지된 얼굴벡터도 함께 샥제 return : 삭제된 사진의 rdsId - public List deletePhotoEsByShareGroupIdList(List shareGroupIdList) { - List fieldValueShareGroupList = shareGroupIdList.stream() - .map(FieldValue::of) - .toList(); + public List deletePhotoEsByShareGroupId(Long shareGroupId) { SearchResponse response = null; List rdsIdList = new ArrayList<>(); List photoNameList = new ArrayList<>(); @@ -242,18 +239,18 @@ public List deletePhotoEsByShareGroupIdList(List shareGroupIdList) { .from(0) .size(5000) .query(q -> q - .terms(t -> t + .term(t -> t .field("shareGroupId") - .terms(te -> te.value(fieldValueShareGroupList)) + .value(FieldValue.of(shareGroupId)) ) ), PhotoEs.class); elasticsearchClient.deleteByQuery(d -> d .index("photos_es") .query(q -> q - .terms(t -> t + .term(t -> t .field("shareGroupId") - .terms(te -> te.value(fieldValueShareGroupList)) + .value(FieldValue.of(shareGroupId)) ) ) ); diff --git a/src/main/java/com/umc/naoman/global/error/ErrorResponse.java b/src/main/java/com/umc/naoman/global/error/ErrorResponse.java index 337b1b90..998d094b 100644 --- a/src/main/java/com/umc/naoman/global/error/ErrorResponse.java +++ b/src/main/java/com/umc/naoman/global/error/ErrorResponse.java @@ -15,7 +15,8 @@ public class ErrorResponse { private final int status; private final String code; private final String message; - @JsonInclude(JsonInclude.Include.NON_EMPTY) + // @JsonInclude(JsonInclude.Include.NON_EMPTY) + // 매핑할 값이 없으면 안드로이드 쪽에서 별도로 구현해야 하기 때문에 위 어노테이션 주석 처리 private final List data; diff --git a/src/main/java/com/umc/naoman/global/error/code/JwtErrorCode.java b/src/main/java/com/umc/naoman/global/error/code/JwtErrorCode.java new file mode 100644 index 00000000..3f240747 --- /dev/null +++ b/src/main/java/com/umc/naoman/global/error/code/JwtErrorCode.java @@ -0,0 +1,19 @@ +package com.umc.naoman.global.error.code; + +import com.umc.naoman.global.error.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum JwtErrorCode implements ErrorCode { + AUTHENTICATION_TYPE_IS_NOT_BEARER(400, "EJ000", "인증 타입이 Bearer가 아닙니다."), + ACCESS_TOKEN_IS_EXPIRED(401, "EJ000", "액세스 토큰이 만료되었습니다."), + MEMBER_NOT_FOUND(404, "EJ000", "해당 memberId를 가진 회원이 존재하지 않습니다. 탈퇴한 회원인지 확인해 주세요."), + + ; + + private final int status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/umc/naoman/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/umc/naoman/global/security/filter/JwtAuthenticationFilter.java index cd569633..e78e62ca 100644 --- a/src/main/java/com/umc/naoman/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/umc/naoman/global/security/filter/JwtAuthenticationFilter.java @@ -1,17 +1,30 @@ package com.umc.naoman.global.security.filter; +import com.umc.naoman.global.error.ErrorCode; import com.umc.naoman.global.security.util.JwtUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.List; +import static com.umc.naoman.global.error.code.JwtErrorCode.*; + +@Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String HEALTH_CHECK_URL = "/"; + private static final List EXCLUDE_URL_PATTERN_LIST = List.of( + "/swagger-ui", + "/swagger-resources", + "/v3/api-docs", + "/auth"); private static final String AUTHORIZATION_TYPE = "Bearer "; private static final String AUTHORIZATION_HEADER = "Authorization"; private final JwtUtils jwtUtils; @@ -23,31 +36,50 @@ public JwtAuthenticationFilter(JwtUtils jwtUtils) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorization = request.getHeader(AUTHORIZATION_HEADER); - - if (!validateJwtIsPresent(authorization)) { + if (authorization == null) { + SecurityContextHolder.clearContext(); filterChain.doFilter(request, response); return; } + if (!authorization.startsWith(AUTHORIZATION_TYPE)) { + handleException(request, response, filterChain, AUTHENTICATION_TYPE_IS_NOT_BEARER); + return; + } + String jwt = authorization.substring(AUTHORIZATION_TYPE.length()); - System.out.println("jwt: " + jwt); + log.info("jwt: {}", jwt); + if (jwtUtils.isExpired(jwt)) { - System.out.println("토큰이 만료되었습니다."); - filterChain.doFilter(request, response); + handleException(request, response, filterChain, ACCESS_TOKEN_IS_EXPIRED); + return; + } + + final Authentication authentication; + try { + authentication = jwtUtils.getAuthentication(jwt); + } catch (UsernameNotFoundException e) { + handleException(request, response, filterChain, MEMBER_NOT_FOUND); return; } - Authentication authentication = jwtUtils.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } - private boolean validateJwtIsPresent(String authorization) { - if (authorization == null || !authorization.startsWith(AUTHORIZATION_TYPE)) { -// System.out.println("토큰이 존재하지 않거나, 인증 타입이 Bearer가 아닙니다."); - return false; - } + private void handleException(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain, ErrorCode errorCode) throws ServletException, IOException{ + SecurityContextHolder.clearContext(); + request.setAttribute("authException", errorCode); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + // Swagger 관련 경로를 필터링에서 제외 + return EXCLUDE_URL_PATTERN_LIST.stream() + .anyMatch(urlPattern -> path.startsWith(urlPattern)) || path.equals(HEALTH_CHECK_URL); - return true; } } diff --git a/src/main/java/com/umc/naoman/global/security/handler/CustomAuthenticationEntryPoint.java b/src/main/java/com/umc/naoman/global/security/handler/CustomAuthenticationEntryPoint.java index 0fa4b070..a2552261 100644 --- a/src/main/java/com/umc/naoman/global/security/handler/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/umc/naoman/global/security/handler/CustomAuthenticationEntryPoint.java @@ -1,8 +1,9 @@ package com.umc.naoman.global.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.naoman.global.error.ErrorCode; import com.umc.naoman.global.error.ErrorResponse; -import jakarta.servlet.ServletException; +import com.umc.naoman.global.error.code.JwtErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; @@ -18,17 +19,23 @@ @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper = new ObjectMapper(); + @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException, ServletException { + AuthenticationException authException) throws IOException { + ErrorCode errorCode = (ErrorCode) request.getAttribute("authException"); + if (errorCode == null) { + errorCode = UNAUTHORIZED; + } + response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setStatus(UNAUTHORIZED.getStatus()); + response.setStatus(errorCode.getStatus()); response.setCharacterEncoding(Charset.defaultCharset().name()); ErrorResponse errorResponse = ErrorResponse.builder() .status(response.getStatus()) - .code(UNAUTHORIZED.getMessage()) - .message(authException.getMessage()) + .code(errorCode.getCode()) + .message(errorCode.getMessage()) .data(null) .build(); diff --git a/src/main/java/com/umc/naoman/global/security/handler/OAuth2LoginSuccessHandler.java b/src/main/java/com/umc/naoman/global/security/handler/OAuth2LoginSuccessHandler.java index 26f1aed0..15534444 100644 --- a/src/main/java/com/umc/naoman/global/security/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/com/umc/naoman/global/security/handler/OAuth2LoginSuccessHandler.java @@ -70,16 +70,15 @@ private void handleExistingMemberLogin(HttpServletRequest request, HttpServletRe } // 로그인 성공 처리를 위해 access token, refresh token 발급 - String accessToken = jwtUtils.createJwt(member.getEmail(), role, ACCESS_TOKEN_VALIDITY_IN_SECONDS); + String accessToken = jwtUtils.createJwt(member.getId(), role, ACCESS_TOKEN_VALIDITY_IN_SECONDS); CookieUtils.addCookie(response, ACCESS_TOKEN_KEY, accessToken, ACCESS_TOKEN_VALIDITY_IN_SECONDS.intValue()); - String refreshToken = jwtUtils.createJwt(member.getEmail(), role, REFRESH_TOKEN_VALIDITY_IN_SECONDS); + String refreshToken = jwtUtils.createJwt(member.getId(), role, REFRESH_TOKEN_VALIDITY_IN_SECONDS); refreshTokenService.saveRefreshToken(member.getId(), refreshToken); CookieUtils.addCookie(response, REFRESH_TOKEN_KEY, refreshToken, REFRESH_TOKEN_VALIDITY_IN_SECONDS.intValue()); clearAuthenticationAttributes(request, response); - // 프론트엔드 홈 화면으로 리다이렉션 - response.sendRedirect(FRONTEND_BASE_URL); + response.sendRedirect(FRONTEND_BASE_URL); // 홈 화면으로 리다이렉션 } private void handleMemberSignup(HttpServletRequest request, HttpServletResponse response, OAuthAttribute oAuthAttribute) @@ -89,8 +88,7 @@ private void handleMemberSignup(HttpServletRequest request, HttpServletResponse CookieUtils.addCookie(response, TEMP_MEMBER_INFO_KEY, tempMemberInfo, TEMP_MEMBER_INFO_VALIDITY_IN_SECONDS.intValue()); clearAuthenticationAttributes(request, response); - // 약관 동의 화면으로 리다이렉션 - response.sendRedirect(FRONTEND_BASE_URL + FRONTEND_AGREEMENT_PATH); + response.sendRedirect(FRONTEND_BASE_URL + FRONTEND_AGREEMENT_PATH); // 약관 동의 화면으로 리다이렉션 } private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) { diff --git a/src/main/java/com/umc/naoman/global/security/service/MemberDetailsService.java b/src/main/java/com/umc/naoman/global/security/service/MemberDetailsService.java index 4c093eaf..f8f3b634 100644 --- a/src/main/java/com/umc/naoman/global/security/service/MemberDetailsService.java +++ b/src/main/java/com/umc/naoman/global/security/service/MemberDetailsService.java @@ -18,10 +18,16 @@ public class MemberDetailsService implements UserDetailsService { private final MemberRepository memberRepository; + /** + * + * @param username 회원을 식별하기 위한 데이터. PK 값인 memberId + * @return + * @throws UsernameNotFoundException + */ @Override public MemberDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(username) - .orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 회원이 존재하지 않습니다.")); + Member member = memberRepository.findById(Long.parseLong(username)) // 전달된 memberId를 Long 타입으로 변환 + .orElseThrow(() -> new UsernameNotFoundException("해당 memberId를 가진 회원이 존재하지 않습니다.")); return new MemberDetails(member); } diff --git a/src/main/java/com/umc/naoman/global/security/util/JwtUtils.java b/src/main/java/com/umc/naoman/global/security/util/JwtUtils.java index ec2dab2c..884de847 100644 --- a/src/main/java/com/umc/naoman/global/security/util/JwtUtils.java +++ b/src/main/java/com/umc/naoman/global/security/util/JwtUtils.java @@ -37,39 +37,13 @@ public class JwtUtils { this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SIGNATURE_ALGORITHM); } - public String getEmail(String token) { - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload() - .get(PAYLOAD_EMAIL_KEY, String.class); - } - - public Claims getPayload(String token) { - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload(); - } - - public Boolean isExpired(String token) { - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload() - .getExpiration().before(new Date()); - } - - public String createJwt(String email, String role, Long seconds) { + public String createJwt(Long memberId, String role, Long seconds) { final LocalDateTime now = LocalDateTime.now(); final Date issuedDate = localDateTimeToDate(now); final Date expiredDate = localDateTimeToDate(now.plusSeconds(seconds)); return Jwts.builder() - .claim(PAYLOAD_EMAIL_KEY, email) + .claim(PAYLOAD_MEMBER_ID_KEY, memberId.toString()) // String 타입으로 세팅 .claim(PAYLOAD_ROLE_KEY, role) .issuedAt(issuedDate) .expiration(expiredDate) @@ -94,12 +68,39 @@ public String createTempMemberInfoJwt(OAuthAttribute oAuthAttribute, Long second .compact(); } + public Claims getPayload(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + // Long 타입이지만 JWT 내부에는 String으로 담겨 있다. + public String getMemberId(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get(PAYLOAD_MEMBER_ID_KEY, String.class); + } + + public Boolean isExpired(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .getExpiration().before(new Date()); + } + public Authentication getAuthentication(String token) { // 나ㅇ만 서비스는 현재 Member 엔티티에게 권한이 존재하지 않으므로 authorities는 빈 리스트 처리 final List authorities = Collections.emptyList(); // 사용자 정의로 구현한 MemberDetails 사용 - final MemberDetails principal = memberDetailsService.loadUserByUsername(getEmail(token)); + final MemberDetails principal = memberDetailsService.loadUserByUsername(getMemberId(token)); return new UsernamePasswordAuthenticationToken(principal, token, authorities); }