From 4e5b8c9cb86cfa142cf916d79de2bbdd30a75335 Mon Sep 17 00:00:00 2001 From: JeongInJae <93825184+injae-348@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:10:36 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 로그인 입력 받아오는 DTO 생성 * feat(PasswordEncoder): 주석 추가 및 비밀 번호 검증 로직 구현 * feat(MemberService): 로그인 정보로 멤버 조회 및 비밀번호 검증 로직 구현 * feat(MemberService): 비밀번호 불일치 예외 처리 * feat(AuthService): 입력 받은 로그인 정보로 JWT 토큰 방식 로그인 구현 * feat(AuthController): 로그인 정보 입력 받는 컨트롤러 구현 * feat(Auth): refreshToken 재발급 서비스 및 컨트롤러 구현 * feat(AuthService): @Transactional 적용 --- .../auth/controller/AuthController.java | 20 +++++++++++++++++++ .../buddybridge/auth/dto/LoginReqDto.java | 15 ++++++++++++++ .../buddybridge/auth/service/AuthService.java | 18 +++++++++++++++++ .../auth/utils/PasswordEncoder.java | 18 +++++++++++++++++ .../exception/InvalidPasswordException.java | 12 +++++++++++ .../member/exception/MemberErrorCode.java | 2 +- .../member/service/MemberService.java | 13 ++++++++++++ 7 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/main/java/econo/buddybridge/auth/dto/LoginReqDto.java create mode 100644 src/main/java/econo/buddybridge/member/exception/InvalidPasswordException.java diff --git a/src/main/java/econo/buddybridge/auth/controller/AuthController.java b/src/main/java/econo/buddybridge/auth/controller/AuthController.java index e35e1da..fbfb98f 100644 --- a/src/main/java/econo/buddybridge/auth/controller/AuthController.java +++ b/src/main/java/econo/buddybridge/auth/controller/AuthController.java @@ -1,5 +1,8 @@ package econo.buddybridge.auth.controller; +import econo.buddybridge.auth.dto.LoginReqDto; +import econo.buddybridge.auth.jwt.AuthToken; +import econo.buddybridge.auth.resolver.MemberToken; import econo.buddybridge.auth.service.AuthService; import econo.buddybridge.common.annotation.AllowAnonymous; import econo.buddybridge.member.dto.MemberSignUpReqDto; @@ -34,4 +37,21 @@ public ApiResponse> signUp( MemberSignUpResDto memberSignUpResDto = authService.signUp(memberSignUpReqDto); return ApiResponseGenerator.success(memberSignUpResDto, HttpStatus.OK); } + + @Operation(summary = "자체 로그인 (JWT)", description = "이메일과 비밀번호를 이용해 JWT 토큰을 발급합니다.") + @PostMapping("/login") + @AllowAnonymous + public ApiResponse> login( + @Valid @RequestBody LoginReqDto params + ) { + AuthToken authToken = authService.loginWithToken(params); + return ApiResponseGenerator.success(authToken, HttpStatus.OK); + } + + @Operation(summary = "Access Token, Refresh Token 재발급", description = "Refresh Token을 이용해 Access Token과 Refresh Token을 재발급합니다.") + @PostMapping("/reissue") + public ApiResponse> reissue(@MemberToken String token) { + AuthToken authToken = authService.reissue(token); + return ApiResponseGenerator.success(authToken, HttpStatus.OK); + } } diff --git a/src/main/java/econo/buddybridge/auth/dto/LoginReqDto.java b/src/main/java/econo/buddybridge/auth/dto/LoginReqDto.java new file mode 100644 index 0000000..973c5bf --- /dev/null +++ b/src/main/java/econo/buddybridge/auth/dto/LoginReqDto.java @@ -0,0 +1,15 @@ +package econo.buddybridge.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginReqDto( + @NotBlank(message = "이메일을 입력해주세요.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + String email, + + @NotBlank(message = "비밀번호를 입력해주세요.") + String password +) { + +} diff --git a/src/main/java/econo/buddybridge/auth/service/AuthService.java b/src/main/java/econo/buddybridge/auth/service/AuthService.java index 55381c9..136189e 100644 --- a/src/main/java/econo/buddybridge/auth/service/AuthService.java +++ b/src/main/java/econo/buddybridge/auth/service/AuthService.java @@ -1,18 +1,36 @@ package econo.buddybridge.auth.service; +import econo.buddybridge.auth.dto.LoginReqDto; +import econo.buddybridge.auth.jwt.AuthToken; +import econo.buddybridge.auth.jwt.service.AuthTokenService; +import econo.buddybridge.member.dto.MemberResDto; import econo.buddybridge.member.dto.MemberSignUpReqDto; import econo.buddybridge.member.dto.MemberSignUpResDto; import econo.buddybridge.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class AuthService { private final MemberService memberService; + private final AuthTokenService authTokenService; + @Transactional public MemberSignUpResDto signUp(MemberSignUpReqDto memberSignUpReqDto) { return memberService.createSignUpMember(memberSignUpReqDto); } + + @Transactional + public AuthToken loginWithToken(LoginReqDto params) { + MemberResDto member = memberService.findMemberByEmailAndPassword(params); + return authTokenService.generateAuthToken(member.memberId()); + } + + @Transactional + public AuthToken reissue(String refreshToken) { + return authTokenService.reissue(refreshToken); + } } diff --git a/src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java b/src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java index e62234c..c860264 100644 --- a/src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java +++ b/src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java @@ -8,6 +8,7 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; @@ -17,6 +18,16 @@ @Slf4j @Component public class PasswordEncoder { + + /* + * 사용자가 입력한 비밀번호, 저장된 비밀번호(해싱됨), 저장된 솔트(암호화) 를 입력 받아 + * 비밀번호의 일치 여부를 확인합니다. + * */ + public boolean verify(String password, String storedPassword, String storedSalt) { + byte[] salt = Base64.getDecoder().decode(storedSalt); + String hashedKey = hashPassword(password, salt); + return MessageDigest.isEqual(storedPassword.getBytes(), hashedKey.getBytes()); + } public PasswordHashDto encrypt(String password) { byte[] salt = generateRandomSalt(); @@ -26,6 +37,9 @@ public PasswordHashDto encrypt(String password) { return new PasswordHashDto(hashedPassword, saltString); } + /* + * 사용자가 입력한 비밀번호와 솔트를 이용해 해싱된 비밀번호를 반환합니다. + * */ private String hashPassword(String password, byte[] salt) { try { KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10000, 128); @@ -38,6 +52,10 @@ private String hashPassword(String password, byte[] salt) { } } + /* + * 솔트를 생성합니다 + * 솔트 : 암호화된 비밀번호를 해독하는데 사용되는 임의의 바이트 배열 + * */ private byte[] generateRandomSalt() { try { SecureRandom random = new SecureRandom(); diff --git a/src/main/java/econo/buddybridge/member/exception/InvalidPasswordException.java b/src/main/java/econo/buddybridge/member/exception/InvalidPasswordException.java new file mode 100644 index 0000000..afe112a --- /dev/null +++ b/src/main/java/econo/buddybridge/member/exception/InvalidPasswordException.java @@ -0,0 +1,12 @@ +package econo.buddybridge.member.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class InvalidPasswordException extends BusinessException { + + public static BusinessException EXCEPTION = new InvalidPasswordException(); + + private InvalidPasswordException() { + super(MemberErrorCode.INVALID_PASSWORD); + } +} diff --git a/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java b/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java index ae3992e..c43af0e 100644 --- a/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java +++ b/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java @@ -6,7 +6,7 @@ public enum MemberErrorCode implements ErrorCode { MEMBER_NOT_FOUND("M001", HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), MEMBER_EMAIL_ALREADY_EXISTS("M002", HttpStatus.BAD_REQUEST, "이미 사용중인 이메일입니다."), - ; + INVALID_PASSWORD("M003", HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."); private final String code; private final HttpStatus httpStatus; diff --git a/src/main/java/econo/buddybridge/member/service/MemberService.java b/src/main/java/econo/buddybridge/member/service/MemberService.java index 8b2ce57..1a7c2fe 100644 --- a/src/main/java/econo/buddybridge/member/service/MemberService.java +++ b/src/main/java/econo/buddybridge/member/service/MemberService.java @@ -1,5 +1,6 @@ package econo.buddybridge.member.service; +import econo.buddybridge.auth.dto.LoginReqDto; import econo.buddybridge.auth.dto.PasswordHashDto; import econo.buddybridge.auth.dto.kakao.UserInfoWithKakaoToken; import econo.buddybridge.auth.utils.PasswordEncoder; @@ -9,6 +10,7 @@ import econo.buddybridge.member.dto.MemberSignUpResDto; import econo.buddybridge.member.entity.DisabilityType; import econo.buddybridge.member.entity.Member; +import econo.buddybridge.member.exception.InvalidPasswordException; import econo.buddybridge.member.exception.MemberEmailAlreadyExistsException; import econo.buddybridge.member.exception.MemberNotFoundException; import econo.buddybridge.member.repository.MemberRepository; @@ -97,4 +99,15 @@ private void signUpMember(MemberSignUpReqDto memberSignUpReqDto) { memberRepository.save(member); } + @Transactional + public MemberResDto findMemberByEmailAndPassword(LoginReqDto loginReqDto) { + Member member = memberRepository.findByEmail(loginReqDto.email()) + .orElseThrow(() -> MemberNotFoundException.EXCEPTION); + + if (!passwordEncoder.verify(loginReqDto.password(), member.getPassword(), member.getSalt())) { + throw InvalidPasswordException.EXCEPTION; + } + + return new MemberResDto(member); + } }