Skip to content

Commit

Permalink
[FEAT] 로그인 기능 구현 (#195)
Browse files Browse the repository at this point in the history
* feat: 로그인 입력 받아오는 DTO 생성

* feat(PasswordEncoder): 주석 추가 및 비밀 번호 검증 로직 구현

* feat(MemberService): 로그인 정보로 멤버 조회 및 비밀번호 검증 로직 구현

* feat(MemberService): 비밀번호 불일치 예외 처리

* feat(AuthService): 입력 받은 로그인 정보로 JWT 토큰 방식 로그인 구현

* feat(AuthController): 로그인 정보 입력 받는 컨트롤러 구현

* feat(Auth): refreshToken 재발급 서비스 및 컨트롤러 구현

* feat(AuthService): @transactional 적용
  • Loading branch information
injae-348 authored Nov 17, 2024
1 parent 9245692 commit 4e5b8c9
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,4 +37,21 @@ public ApiResponse<CustomBody<MemberSignUpResDto>> signUp(
MemberSignUpResDto memberSignUpResDto = authService.signUp(memberSignUpReqDto);
return ApiResponseGenerator.success(memberSignUpResDto, HttpStatus.OK);
}

@Operation(summary = "자체 로그인 (JWT)", description = "이메일과 비밀번호를 이용해 JWT 토큰을 발급합니다.")
@PostMapping("/login")
@AllowAnonymous
public ApiResponse<CustomBody<AuthToken>> 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<CustomBody<AuthToken>> reissue(@MemberToken String token) {
AuthToken authToken = authService.reissue(token);
return ApiResponseGenerator.success(authToken, HttpStatus.OK);
}
}
15 changes: 15 additions & 0 deletions src/main/java/econo/buddybridge/auth/dto/LoginReqDto.java
Original file line number Diff line number Diff line change
@@ -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
) {

}
18 changes: 18 additions & 0 deletions src/main/java/econo/buddybridge/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -38,6 +52,10 @@ private String hashPassword(String password, byte[] salt) {
}
}

/*
* 솔트를 생성합니다
* 솔트 : 암호화된 비밀번호를 해독하는데 사용되는 임의의 바이트 배열
* */
private byte[] generateRandomSalt() {
try {
SecureRandom random = new SecureRandom();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/econo/buddybridge/member/service/MemberService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 4e5b8c9

Please sign in to comment.