From 1ce08cb981d037d47961cdd5fcd4e6529adc3655 Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Fri, 22 Aug 2025 12:56:25 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=9A=8C=EC=9B=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20refresh=5Ftoken=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EC=A6=9D=EA=B0=80=20=EB=B0=8F=20MemberMap?= =?UTF-8?q?per=EC=97=90=20DTO=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80.=20MemberServi?= =?UTF-8?q?ce=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0,=20JWT=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=97=90=EC=84=9C=20refresh=5Ftoken=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddl/schema.sql | 2 +- .../domain/base/member/entity/Member.java | 2 +- .../base/member/mapper/MemberMapper.java | 10 ++++ .../member/repository/MemberRepository.java | 16 ++++++ .../base/member/service/MemberService.java | 3 +- .../member/service/MemberServiceImpl.java | 53 +++++++------------ .../global/filter/JwtTokenIssuanceFilter.java | 20 ++++--- .../global/security/WebSecurity.java | 2 +- .../bio_backend/global/utils/JwtUtils.java | 17 +----- 9 files changed, 63 insertions(+), 62 deletions(-) diff --git a/ddl/schema.sql b/ddl/schema.sql index 82624fe..c8ddb0f 100644 --- a/ddl/schema.sql +++ b/ddl/schema.sql @@ -11,7 +11,7 @@ name varchar(100) not null, password varchar(100) not null, user_id varchar(100) not null, - refresh_token varchar(200), + refresh_token varchar(1024), email varchar(255) not null, primary key (oid) ); diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java b/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java index 2790ae5..f265c83 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/entity/Member.java @@ -44,7 +44,7 @@ public class Member extends BaseEntity { @Builder.Default private Boolean useFlag = true; - @Column(name = "refresh_token", length = 200) + @Column(name = "refresh_token", length = 1024) private String refreshToken; @Column(name = "last_login_at") diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java b/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java index 5a04987..e9ebedf 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/mapper/MemberMapper.java @@ -46,4 +46,14 @@ public interface MemberMapper { * MemberDto를 CreateMemberResponseDto로 변환 */ CreateMemberResponseDto toCreateMemberResponseDto(MemberDto memberDto); + + /** + * MemberDto의 값으로 기존 Member 엔티티 업데이트 (null이 아닌 필드만) + */ + @Mapping(target = "oid", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "createdOid", ignore = true) + @Mapping(target = "updatedOid", ignore = true) + void updateMemberFromDto(MemberDto memberDto, @org.mapstruct.MappingTarget Member member); } diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/repository/MemberRepository.java b/src/main/java/com/bio/bio_backend/domain/base/member/repository/MemberRepository.java index f83d09a..8ca52d6 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/repository/MemberRepository.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/repository/MemberRepository.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; import com.bio.bio_backend.domain.base.member.entity.Member; import java.util.Optional; +import java.util.List; @Repository public interface MemberRepository extends JpaRepository { @@ -12,6 +13,21 @@ public interface MemberRepository extends JpaRepository { // 사용자 ID로 회원 조회 (Optional 반환) Optional findByUserId(String userId); + // 사용자 ID로 첫 번째 회원 조회 (userId가 unique하지 않을 경우 대비) + Optional findFirstByUserId(String userId); + + // 사용자 ID로 활성화된 회원 조회 + Optional findByUserIdAndUseFlagTrue(String userId); + + // 사용자 ID로 활성화된 회원 첫 번째 조회 + Optional findFirstByUserIdAndUseFlagTrue(String userId); + + // 사용자 ID 존재 여부 확인 (활성화된 회원만) + boolean existsByUserIdAndUseFlagTrue(String userId); + // 사용자 ID 존재 여부 확인 boolean existsByUserId(String userId); + + // 활성화된 회원 목록 조회 + List findByUseFlagTrue(); } \ No newline at end of file diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java index e8beb19..134daa4 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberService.java @@ -1,5 +1,6 @@ package com.bio.bio_backend.domain.base.member.service; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -14,8 +15,6 @@ public interface MemberService extends UserDetailsService { MemberDto createMember(MemberDto memberDTO); - void updateRefreshToken(MemberDto memberDTO); - String getRefreshToken(String id); int deleteRefreshToken(String id); diff --git a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberServiceImpl.java b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberServiceImpl.java index f6491db..66ff530 100644 --- a/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberServiceImpl.java +++ b/src/main/java/com/bio/bio_backend/domain/base/member/service/MemberServiceImpl.java @@ -32,7 +32,7 @@ public class MemberServiceImpl implements MemberService { @Override public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { - Member member = memberRepository.findByUserId(id) + Member member = memberRepository.findByUserIdAndUseFlagTrue(id) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + id)); // MapStruct를 사용하여 Member 엔티티를 MemberDto로 변환 @@ -45,7 +45,7 @@ public class MemberServiceImpl implements MemberService { @Transactional public MemberDto createMember(MemberDto memberDto) { // userId 중복 체크 - if (memberRepository.existsByUserId(memberDto.getUserId())) { + if (memberRepository.existsByUserIdAndUseFlagTrue(memberDto.getUserId())) { throw new ApiException(ApiResponseCode.USER_ID_DUPLICATE); } @@ -68,19 +68,24 @@ public class MemberServiceImpl implements MemberService { @Override @Transactional - public void updateRefreshToken(MemberDto memberDTO) { - // JPA를 사용하여 refresh token 업데이트 - Member member = memberRepository.findByUserId(memberDTO.getUserId()) - .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDTO.getUserId())); + public int updateMember(MemberDto memberDto) { + Member member = memberRepository.findFirstByUserIdAndUseFlagTrue(memberDto.getUserId()) + .orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND)); + + memberMapper.updateMemberFromDto(memberDto, member); + + if (memberDto.getPassword() != null && !memberDto.getPassword().isEmpty()) { + member.setPassword(bCryptPasswordEncoder.encode(memberDto.getPassword())); + } - member.setRefreshToken(memberDTO.getRefreshToken()); memberRepository.save(member); + return 1; } @Override public String getRefreshToken(String id) { - Member member = memberRepository.findByUserId(id) - .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + id)); + Member member = memberRepository.findFirstByUserIdAndUseFlagTrue(id) + .orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND)); return member.getRefreshToken(); } @@ -88,8 +93,8 @@ public class MemberServiceImpl implements MemberService { @Override @Transactional public int deleteRefreshToken(String id) { - Member member = memberRepository.findByUserId(id) - .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + id)); + Member member = memberRepository.findFirstByUserIdAndUseFlagTrue(id) + .orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND)); member.setRefreshToken(null); memberRepository.save(member); @@ -98,10 +103,8 @@ public class MemberServiceImpl implements MemberService { @Override public List selectMemberList(Map params) { - // JPA를 사용하여 회원 목록 조회 (간단한 구현) - List members = memberRepository.findAll(); + List members = memberRepository.findByUseFlagTrue(); - // MapStruct를 사용하여 Member 엔티티 리스트를 MemberDto 리스트로 변환 return memberMapper.toMemberDtoList(members); } @@ -115,29 +118,13 @@ public class MemberServiceImpl implements MemberService { return memberMapper.toMemberDto(member); } - @Override - @Transactional - public int updateMember(MemberDto memberDto) { - Member member = memberRepository.findByUserId(memberDto.getUserId()) - .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDto.getUserId())); - - // 비밀번호가 변경된 경우 암호화 - if (memberDto.getPassword() != null && !memberDto.getPassword().isEmpty()) { - member.setPassword(bCryptPasswordEncoder.encode(memberDto.getPassword())); - } - - member.setRole(memberDto.getRole()); - member.setUseFlag(memberDto.getUseFlag()); - - memberRepository.save(member); - return 1; // 성공 시 1 반환 - } + @Override @Transactional public int deleteMember(MemberDto memberDto) { - Member member = memberRepository.findByUserId(memberDto.getUserId()) - .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDto.getUserId())); + Member member = memberRepository.findFirstByUserId(memberDto.getUserId()) + .orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND)); member.setUseFlag(false); diff --git a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java index 5dd8c22..641e6a3 100644 --- a/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java @@ -9,14 +9,15 @@ import com.bio.bio_backend.global.dto.ApiResponseDto; import com.bio.bio_backend.domain.base.member.dto.LoginRequestDto; import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto; import com.bio.bio_backend.domain.base.member.dto.MemberDto; +import com.bio.bio_backend.domain.base.member.service.MemberService; import com.bio.bio_backend.global.constants.ApiResponseCode; import com.bio.bio_backend.global.utils.JwtUtils; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -36,15 +37,20 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter private final AuthenticationManager authenticationManager; private final JwtUtils jwtUtils; private final ObjectMapper objectMapper; + private final MemberService memberService; // 사용자 login 인증 처리 - @SneakyThrows @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.getUserId(), requestDto.getPassword()); + try { + LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + requestDto.getUserId(), requestDto.getPassword()); - return authenticationManager.authenticate(authToken); + return authenticationManager.authenticate(authToken); + } catch (IOException e) { + throw new AuthenticationServiceException("로그인 요청 파싱 중 오류가 발생했습니다", e); + } } // 사용자 인증 성공 후 token 발급 @@ -59,11 +65,9 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter String accessToken = jwtUtils.createAccessToken(member.getUserId(), member.getRole().getValue()); String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue()); - // Refresh 토큰을 DB에 저장 - jwtUtils.saveRefreshToken(member.getUserId(), refreshToken); - member.setRefreshToken(refreshToken); member.setLastLoginAt(LocalDateTime.now()); + memberService.updateMember(member); // Refresh 토큰 쿠키 저장 jwtUtils.setRefreshTokenCookie(response, refreshToken); diff --git a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java index dbcecc0..1cfdbf7 100644 --- a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java +++ b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java @@ -37,7 +37,7 @@ public class WebSecurity { private final SecurityPathConfig securityPathConfig; private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception { - JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper); + JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService); filter.setFilterProcessesUrl("/login"); filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper)); return filter; diff --git a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java index 06b7de2..b74d520 100644 --- a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java +++ b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java @@ -105,25 +105,10 @@ public class JwtUtils { Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh")))); } - // Refresh Token을 DB에 저장 - public void saveRefreshToken(String username, String refreshToken) { - MemberDto member = new MemberDto(); - member.setUserId(username); - member.setRefreshToken(refreshToken); - memberService.updateRefreshToken(member); - } - - // Refresh Token 생성 및 DB 저장 (기존 호환성을 위한 메서드) - public String createAndSaveRefreshToken(String username, String role) { - String refreshToken = createRefreshToken(username, role); - saveRefreshToken(username, refreshToken); - return refreshToken; - } - // Refresh Token 갱신 (Access Token 갱신 시 함께) public String refreshTokens(String username, String role) { // 새로운 Refresh Token 생성 및 DB 저장 - String newRefreshToken = createAndSaveRefreshToken(username, role); + String newRefreshToken = createRefreshToken(username, role); log.info("Refresh Token 갱신 완료: {}", username); return newRefreshToken;