From 89c65499c1b31da635205446bb9382a17e8abf23 Mon Sep 17 00:00:00 2001 From: Soyeon-Cha <7103sy@naver.com> Date: Wed, 22 Nov 2023 13:30:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../Member/controller/MemberController.java | 51 +++++++ .../java/MARKETFUBY/Member/domain/Member.java | 17 +-- .../Member/domain/RefreshToken.java | 24 ++++ .../Member/domain/UseAgreement.java | 15 -- .../Member/dto/MemberJoinRequestDto.java | 17 +++ .../Member/dto/MemberLoginRequestDto.java | 12 ++ .../Member/dto/MemberLoginResponseDto.java | 23 +++ .../Member/dto/RefreshTokenRequestDto.java | 11 ++ .../Member/repository/MemberRepository.java | 8 +- .../repository/RefreshTokenRepository.java | 12 ++ .../Member/service/MemberService.java | 133 ++++++++++++++++++ .../Member/service/RefreshTokenService.java | 31 ++++ src/main/java/MARKETFUBY/utils/JwtUtil.java | 51 +++++++ 14 files changed, 379 insertions(+), 30 deletions(-) create mode 100644 src/main/java/MARKETFUBY/Member/controller/MemberController.java create mode 100644 src/main/java/MARKETFUBY/Member/domain/RefreshToken.java delete mode 100644 src/main/java/MARKETFUBY/Member/domain/UseAgreement.java create mode 100644 src/main/java/MARKETFUBY/Member/dto/MemberJoinRequestDto.java create mode 100644 src/main/java/MARKETFUBY/Member/dto/MemberLoginRequestDto.java create mode 100644 src/main/java/MARKETFUBY/Member/dto/MemberLoginResponseDto.java create mode 100644 src/main/java/MARKETFUBY/Member/dto/RefreshTokenRequestDto.java create mode 100644 src/main/java/MARKETFUBY/Member/repository/RefreshTokenRepository.java create mode 100644 src/main/java/MARKETFUBY/Member/service/MemberService.java create mode 100644 src/main/java/MARKETFUBY/Member/service/RefreshTokenService.java create mode 100644 src/main/java/MARKETFUBY/utils/JwtUtil.java diff --git a/build.gradle b/build.gradle index 1ced392..e274e63 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'io.jsonwebtoken:jjwt:0.9.1' @@ -31,7 +31,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - //testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/MARKETFUBY/Member/controller/MemberController.java b/src/main/java/MARKETFUBY/Member/controller/MemberController.java new file mode 100644 index 0000000..86043e4 --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/controller/MemberController.java @@ -0,0 +1,51 @@ +package MARKETFUBY.Member.controller; + +import MARKETFUBY.Member.dto.MemberJoinRequestDto; +import MARKETFUBY.Member.dto.MemberLoginRequestDto; +import MARKETFUBY.Member.dto.MemberLoginResponseDto; +import MARKETFUBY.Member.dto.RefreshTokenRequestDto; +import MARKETFUBY.Member.service.MemberService; +import MARKETFUBY.Member.service.RefreshTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class MemberController { + private final MemberService memberService; + private final RefreshTokenService refreshTokenService; + + // 회원가입 + @PostMapping("/signup") + public ResponseEntity join (@RequestBody MemberJoinRequestDto requestDto) { + return ResponseEntity.ok().body(memberService.join(requestDto.getFubyId(), requestDto.getPasswd(), requestDto.getName(), requestDto.getEmail(), requestDto.getPhone(), requestDto.getHome(), requestDto.getSex(), requestDto.getBirthday())); + } + + // 로그인 + @PostMapping("/login") + public MemberLoginResponseDto login (@RequestBody MemberLoginRequestDto requestDto) { + return memberService.login(requestDto.getFubyId(), requestDto.getPasswd()); + } + + // 로그아웃 + @DeleteMapping("/logout") + public String logout(@RequestBody RefreshTokenRequestDto requestDto) { + refreshTokenService.deleteRefreshToken(requestDto.getRefreshToken()); + return "로그아웃되었습니다."; + } + + // 회원탈퇴 + @DeleteMapping("/{memberId}") + public ResponseEntity delete(@PathVariable Long memberId, Authentication authentication) { + return ResponseEntity.ok().body(memberService.delete(memberId, authentication)); + } + + // RefreshToken을 이용해 새로운 AccessToken을 발급받기 + @PostMapping("/refreshtoken") + public MemberLoginResponseDto requestRefresh (@RequestBody RefreshTokenRequestDto refreshTokenDto) { + return memberService.requestRefresh(refreshTokenDto.getRefreshToken()); + } +} \ No newline at end of file diff --git a/src/main/java/MARKETFUBY/Member/domain/Member.java b/src/main/java/MARKETFUBY/Member/domain/Member.java index 83fa5b5..3da9b5b 100644 --- a/src/main/java/MARKETFUBY/Member/domain/Member.java +++ b/src/main/java/MARKETFUBY/Member/domain/Member.java @@ -1,17 +1,16 @@ package MARKETFUBY.Member.domain; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import javax.persistence.*; -@Getter -@Setter @Entity +@Getter @Table(name="member") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,13 +34,9 @@ public class Member { private String birthday; @Column private String level; - @Column - private Boolean selectAgreement; - @Column - private UseAgreement useAgreement; @Builder - public Member(String fubyId, String passwd, String name, String email, String phone, String home, Sex sex, String birthday, String level, boolean selectAgreement, UseAgreement useAgreement){ + public Member(String fubyId, String passwd, String name, String email, String phone, String home, Sex sex, String birthday, String level) { this.fubyId = fubyId; this.passwd = passwd; this.name = name; @@ -51,7 +46,5 @@ public Member(String fubyId, String passwd, String name, String email, String ph this.sex = sex; this.birthday = birthday; this.level = level; - this.selectAgreement = selectAgreement; - this. useAgreement = useAgreement; } -} +} \ No newline at end of file diff --git a/src/main/java/MARKETFUBY/Member/domain/RefreshToken.java b/src/main/java/MARKETFUBY/Member/domain/RefreshToken.java new file mode 100644 index 0000000..bdf526f --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/domain/RefreshToken.java @@ -0,0 +1,24 @@ +package MARKETFUBY.Member.domain; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long refreshTokenId; + + @Column(nullable = false) + private Long memberId; + + @Column(nullable = false) + private String value; +} + diff --git a/src/main/java/MARKETFUBY/Member/domain/UseAgreement.java b/src/main/java/MARKETFUBY/Member/domain/UseAgreement.java deleted file mode 100644 index 5d1ce8f..0000000 --- a/src/main/java/MARKETFUBY/Member/domain/UseAgreement.java +++ /dev/null @@ -1,15 +0,0 @@ -package MARKETFUBY.Member.domain; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum UseAgreement { - SMS(0, "SMS"), - MAIL(1, "메일"), - BOTH(2, "둘다"); - - private final int optionId; - private final String option; -} diff --git a/src/main/java/MARKETFUBY/Member/dto/MemberJoinRequestDto.java b/src/main/java/MARKETFUBY/Member/dto/MemberJoinRequestDto.java new file mode 100644 index 0000000..8dd44dc --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/dto/MemberJoinRequestDto.java @@ -0,0 +1,17 @@ +package MARKETFUBY.Member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MemberJoinRequestDto { + private String fubyId; + private String passwd; + private String name; + private String email; + private String phone; + private String home; + private String sex; + private String birthday; +} \ No newline at end of file diff --git a/src/main/java/MARKETFUBY/Member/dto/MemberLoginRequestDto.java b/src/main/java/MARKETFUBY/Member/dto/MemberLoginRequestDto.java new file mode 100644 index 0000000..f06cbfb --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/dto/MemberLoginRequestDto.java @@ -0,0 +1,12 @@ +package MARKETFUBY.Member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MemberLoginRequestDto { + private String fubyId; + private String passwd; +} + diff --git a/src/main/java/MARKETFUBY/Member/dto/MemberLoginResponseDto.java b/src/main/java/MARKETFUBY/Member/dto/MemberLoginResponseDto.java new file mode 100644 index 0000000..fe9e27f --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/dto/MemberLoginResponseDto.java @@ -0,0 +1,23 @@ +package MARKETFUBY.Member.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberLoginResponseDto { + private Long memberId; + private String username; + private String accessToken; + private String refreshToken; + + @Builder + public MemberLoginResponseDto(Long memberId, String fubyId, String accessToken, String refreshToken) { + this.memberId = memberId; + this.username = fubyId; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/MARKETFUBY/Member/dto/RefreshTokenRequestDto.java b/src/main/java/MARKETFUBY/Member/dto/RefreshTokenRequestDto.java new file mode 100644 index 0000000..d7c75b7 --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/dto/RefreshTokenRequestDto.java @@ -0,0 +1,11 @@ +package MARKETFUBY.Member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RefreshTokenRequestDto { + private String refreshToken; +} + diff --git a/src/main/java/MARKETFUBY/Member/repository/MemberRepository.java b/src/main/java/MARKETFUBY/Member/repository/MemberRepository.java index c08a8f7..73eda59 100644 --- a/src/main/java/MARKETFUBY/Member/repository/MemberRepository.java +++ b/src/main/java/MARKETFUBY/Member/repository/MemberRepository.java @@ -1,8 +1,14 @@ package MARKETFUBY.Member.repository; +import MARKETFUBY.Member.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; -import MARKETFUBY.Member.domain.Member; +import java.util.Optional; +@Repository public interface MemberRepository extends JpaRepository { + // fubyId 중복 검사 + Boolean existsByFubyId(String fubyId); + Optional findByFubyId(String fubyId); } diff --git a/src/main/java/MARKETFUBY/Member/repository/RefreshTokenRepository.java b/src/main/java/MARKETFUBY/Member/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..c964858 --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/repository/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package MARKETFUBY.Member.repository; + +import MARKETFUBY.Member.domain.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RefreshTokenRepository extends JpaRepository { + Optional findByValue(String value); +} diff --git a/src/main/java/MARKETFUBY/Member/service/MemberService.java b/src/main/java/MARKETFUBY/Member/service/MemberService.java new file mode 100644 index 0000000..ead8260 --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/service/MemberService.java @@ -0,0 +1,133 @@ +package MARKETFUBY.Member.service; + +import MARKETFUBY.Member.domain.Member; +import MARKETFUBY.Member.domain.RefreshToken; +import MARKETFUBY.Member.domain.Sex; +import MARKETFUBY.Member.dto.MemberLoginResponseDto; +import MARKETFUBY.Member.repository.MemberRepository; +import MARKETFUBY.utils.JwtUtil; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityNotFoundException; + +@Service +@RequiredArgsConstructor +public class MemberService { + private final MemberRepository memberRepository; + private final BCryptPasswordEncoder encoder; + private final RefreshTokenService refreshTokenService; + + // AccessToken 만료 시간을 1시간으로 설정 + private Long AccessExpireTimeMs = 1000 * 60 * 60L; + // RefreshToken 만료 시간을 7일로 설정 + private Long RefreshExpireTimeMs = 7 * 24 * 1000 * 60 * 60L; + + // application-secret.yml 에서 키값 가져오기 + @Value("${spring.jwt.secret-key}") + private String accessKey; + @Value("${spring.jwt.refresh-key}") + private String refreshKey; + + // 회원가입 + public String join(String fubyId, String passwd, String name, String email, String phone, String home, String sex, String birthday) { + // fubyId 중복 체크 + if(existsByFubyId(fubyId)) throw new RuntimeException(fubyId + "은 이미 존재하는 아이디입니다!"); + + // 중복되지 않는다면 저장 + memberRepository.save( + Member.builder() + .fubyId(fubyId) + .passwd(encoder.encode(passwd)) + .name(name) + .email(email) + .phone(phone) + .home(home) + .sex(Sex.valueOf(sex)) + .birthday(birthday) + .level("프렌즈") + .build() + ); + return "성공적으로 회원가입되었습니다!"; + } + + @Transactional(readOnly = true) + public boolean existsByFubyId(String fubyId) { + return memberRepository.existsByFubyId(fubyId); + } + + // 로그인 + public MemberLoginResponseDto login(String fubyId, String passwd) { + // 존재하지 않는 fubyId로 로그인을 시도하는 경우 + Member foundMember = findMemberByFubyId(fubyId); + + // 존재하는 fubyId를 입력했지만 잘못된 비밀번호를 입력하는 경우 + if(!encoder.matches(passwd, foundMember.getPasswd())) throw new RuntimeException("잘못된 비밀번호입니다."); + + // 로그인 성공 -> 토큰 생성 + String accessToken = JwtUtil.createAccessToken(foundMember.getFubyId(), accessKey, AccessExpireTimeMs); + String refreshToken = JwtUtil.createRefreshToken(foundMember.getFubyId(), refreshKey, RefreshExpireTimeMs); + + // RefreshToken을 DB에 저장 + RefreshToken refreshTokenEntity = new RefreshToken(); + refreshTokenEntity.setValue(refreshToken); + refreshTokenEntity.setMemberId(foundMember.getMemberId()); + refreshTokenService.addRefreshToken(refreshTokenEntity); + + return MemberLoginResponseDto.builder() + .memberId(foundMember.getMemberId()) + .fubyId(foundMember.getFubyId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + // 회원탈퇴 + public String delete(Long memberId, Authentication authentication) { + Member member = findMemberById(memberId); + memberRepository.delete(member); + return "성공적으로 탈퇴되었습니다!"; + } + + // fubyId로 회원 찾기 + @Transactional(readOnly = true) + public Member findMemberByFubyId(String fubyId) { + return memberRepository.findByFubyId(fubyId) + .orElseThrow(() -> new EntityNotFoundException(fubyId + "은 존재하지 않는 아이디입니다.")); + } + + // memberId로 Member 정보 찾기 + @Transactional(readOnly = true) + public Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Member ID가 " + id + "인 회원이 존재하지 않습니다.")); + } + + // 새로운 AccessToken 발급받기 + public MemberLoginResponseDto requestRefresh(String refreshToken) { + // 해당 RefreshToken이 유효한지 DB에서 탐색 + RefreshToken foundRefreshToken = refreshTokenService.findRefreshToken(refreshToken); + // RefreshToken에 들어있는 username 값 가져오기 + Claims claims = JwtUtil.parseRefreshToken(foundRefreshToken.getValue(), refreshKey); + String fubyId = claims.get("username").toString(); + System.out.println("Username found in RefreshToken: " + fubyId); + // 가져온 fubyId에 해당하는 회원이 존재하는지 확인 + Member member = findMemberByFubyId(fubyId); + + // 새 AccessKey 생성 + String accessToken = JwtUtil.createAccessToken(member.getFubyId(), accessKey, AccessExpireTimeMs); + // 새 AccessKey와 기존 RefreshKey를 DTO에 담아 리턴 + return MemberLoginResponseDto + .builder() + .memberId(member.getMemberId()) + .fubyId(member.getFubyId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/MARKETFUBY/Member/service/RefreshTokenService.java b/src/main/java/MARKETFUBY/Member/service/RefreshTokenService.java new file mode 100644 index 0000000..545a242 --- /dev/null +++ b/src/main/java/MARKETFUBY/Member/service/RefreshTokenService.java @@ -0,0 +1,31 @@ +package MARKETFUBY.Member.service; + +import MARKETFUBY.Member.domain.RefreshToken; +import MARKETFUBY.Member.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityNotFoundException; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + + public RefreshToken addRefreshToken(RefreshToken refreshToken) { + return refreshTokenRepository.save(refreshToken); + } + + @Transactional(readOnly = true) + public RefreshToken findRefreshToken(String refreshToken) { + return refreshTokenRepository.findByValue(refreshToken) + .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 RefreshToken입니다.")); + } + + public void deleteRefreshToken(String refreshToken) { + RefreshToken foundRefreshToken = refreshTokenRepository.findByValue(refreshToken) + .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 RefreshToken입니다.")); + refreshTokenRepository.delete(foundRefreshToken); + } +} diff --git a/src/main/java/MARKETFUBY/utils/JwtUtil.java b/src/main/java/MARKETFUBY/utils/JwtUtil.java new file mode 100644 index 0000000..18d0e92 --- /dev/null +++ b/src/main/java/MARKETFUBY/utils/JwtUtil.java @@ -0,0 +1,51 @@ +package MARKETFUBY.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.util.Date; + +public class JwtUtil { + // token이 만료되면 true를 리턴 + public static boolean isExpired(String token, String secretKey) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) + .getBody().getExpiration().before(new Date()); + } + + // token에서 userName을 가져와 리턴 + public static String getUserName(String token, String secretKey) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) + .getBody().get("userName", String.class); + } + + // token 생성 (createAccessToken과 createRefreshToken이 이 함수를 호출) + public static String createToken (String fubyId, String key, long expireTimeMs) { + Claims claims = Jwts.claims(); // token 생성에 필요한 데이터를 담아두는 공간 + claims.put("fubyId", fubyId); // 회원의 아이디(fubyId) 저장 + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시각 + .setExpiration(new Date(System.currentTimeMillis() + expireTimeMs)) // 만료 시각 + .signWith(SignatureAlgorithm.HS256, key) // HS256이라는 알고리즘과 주어진 key를 이용해 암호화 + .compact(); + } + + // AccessToken 생성 + public static String createAccessToken(String userName, String key, long expireTimeMs) { + return createToken(userName, key, expireTimeMs); + } + + // RefreshToken 생성 + public static String createRefreshToken (String userName, String key, long expireTimeMs) { + return createToken(userName, key, expireTimeMs); + } + + public static Claims parseRefreshToken(String value, String key) { + return Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(value) + .getBody(); + } +}