-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* style: java.net.URI import 위치 변경 * rename: AuthController -> OAuthController 이름 변경 * feat: 회원가입 요청, 응답 Dto 생성 * feat: 멤버 엔티티 비밀번호, 솔트 필드 추가 및 입력받은 회원가입 정보로 회원 생성 * feat: 비밀번호 인코더 예외 코드 작성 * feat: 비밀번호 인코더 구현 * feat: AuthController 및 AuthService 회원가입 기능 구현 * feat: 회원가입 Dto 검증 적용 * style: findOrCreateSignUpMemberByEmail -> createSignUpMember 메서드명 변경 * refactor(MemberSignUpResDto): 응답 필드 변경 * feat: 가입된 이메일이 있는 경우 예외 처리 * refactor: 존재하는 회원 -> 존재하는 메일로 변경 * refactor: 존재하는 -> 사용중인 에러메시지 변경 * refactor(MemberErrorCode): 사용중인 메일입니다 -> 사용중인 이메일입니다 에러 메시지 변경 * fix(MemberService): 회원가입시 암호화 두번 되던 오류 수정 * refactor(PasswordEncoder): 암호화 알고리즘 SHA1에서 SHA256 변경 * refactor(PasswordHashDto): PasswordHashDto 분리 * style(MemberService): newSignUpMember, signUpMember로 메서드명 변경 * refactor(PasswordEncoder): PBKDF2 반복 횟수 65,536에서 10,000으로 최적화
- Loading branch information
Showing
16 changed files
with
357 additions
and
80 deletions.
There are no files selected for viewing
88 changes: 13 additions & 75 deletions
88
src/main/java/econo/buddybridge/auth/controller/AuthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,99 +1,37 @@ | ||
package econo.buddybridge.auth.controller; | ||
|
||
import econo.buddybridge.auth.OAuthProvider; | ||
import econo.buddybridge.auth.dto.kakao.KakaoLoginParams; | ||
import econo.buddybridge.auth.exception.AlreadyLogoutException; | ||
import econo.buddybridge.auth.jwt.AuthToken; | ||
import econo.buddybridge.auth.resolver.MemberToken; | ||
import econo.buddybridge.auth.service.OAuthLoginService; | ||
import econo.buddybridge.auth.service.AuthService; | ||
import econo.buddybridge.common.annotation.AllowAnonymous; | ||
import econo.buddybridge.member.dto.MemberResDto; | ||
import econo.buddybridge.member.dto.MemberSignUpReqDto; | ||
import econo.buddybridge.member.dto.MemberSignUpResDto; | ||
import econo.buddybridge.utils.api.ApiResponse; | ||
import econo.buddybridge.utils.api.ApiResponse.CustomBody; | ||
import econo.buddybridge.utils.api.ApiResponseGenerator; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpSession; | ||
import java.net.URI; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@Slf4j | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/oauth") | ||
@RequestMapping("/api/auth") | ||
@Tag(name = "인증 API", description = "인증 관련 API") | ||
public class AuthController { | ||
|
||
private final OAuthLoginService oAuthLoginService; | ||
private final AuthService authService; | ||
|
||
@Value("${oauth.kakao.url.front-url}") | ||
private String frontUrl; | ||
|
||
@Operation(summary = "로그아웃", description = "세션을 제거합니다.") | ||
@Operation(summary = "회원 가입", description = "회원을 추가합니다.") | ||
@PostMapping("/signup") | ||
@AllowAnonymous | ||
@PostMapping("/logout") | ||
public ApiResponse<CustomBody<String>> logout(HttpServletRequest request) { | ||
HttpSession session = request.getSession(false); | ||
if (session != null) { | ||
session.invalidate(); | ||
oAuthLoginService.logout(OAuthProvider.KAKAO); | ||
return ApiResponseGenerator.success("로그아웃 성공", HttpStatus.OK); | ||
} | ||
throw AlreadyLogoutException.EXCEPTION; | ||
} | ||
|
||
@Operation(summary = "카카오 소셜 로그인 (코드로 로그인)", description = "Redirect URL이 백엔드 주소로 설정될 때 사용합니다.") | ||
@GetMapping("/login") | ||
public ApiResponse<CustomBody<MemberResDto>> login(@RequestParam("code") String code, HttpServletRequest request) { | ||
KakaoLoginParams params = new KakaoLoginParams(code); | ||
|
||
MemberResDto memberDto = oAuthLoginService.login(params); | ||
|
||
HttpSession session = request.getSession(true); | ||
session.setAttribute("memberId", memberDto.memberId()); | ||
|
||
// 프론트엔드 주소로 redirect | ||
HttpHeaders httpHeaders = new HttpHeaders(); | ||
httpHeaders.setLocation(URI.create(frontUrl)); | ||
|
||
return ApiResponseGenerator.success(memberDto, httpHeaders, HttpStatus.PERMANENT_REDIRECT); | ||
} | ||
|
||
@Operation(summary = "카카오 소셜 로그인 (토큰으로 로그인)", description = "Redirect URL이 프론트엔드 주소로 설정될 때 사용합니다.") | ||
@PostMapping("/login") | ||
public ApiResponse<CustomBody<MemberResDto>> login(@RequestBody KakaoLoginParams params, HttpServletRequest request) { | ||
MemberResDto memberDto = oAuthLoginService.login(params); | ||
|
||
HttpSession session = request.getSession(true); | ||
session.setAttribute("memberId", memberDto.memberId()); | ||
|
||
return ApiResponseGenerator.success(memberDto, HttpStatus.OK); | ||
} | ||
|
||
// 소셜로그인 with JWT | ||
@Operation(summary = "카카오 소셜 로그인 (JWT)", description = "JWT를 이용하여 로그인합니다.") | ||
@PostMapping("/login/jwt") | ||
public ApiResponse<CustomBody<AuthToken>> loginWithToken(@RequestBody KakaoLoginParams params) { | ||
AuthToken authToken = oAuthLoginService.loginWithToken(params); | ||
return ApiResponseGenerator.success(authToken, HttpStatus.OK); | ||
} | ||
|
||
// refresh token 재발급 | ||
@Operation(summary = "Access Token, Refresh Token 재발급", description = "Refresh Token을 이용하여 두 토큰 모두 재발급합니다.") | ||
@PostMapping("/reissue") | ||
public ApiResponse<CustomBody<AuthToken>> reissue(@MemberToken String token) { | ||
AuthToken authToken = oAuthLoginService.reissue(token); | ||
return ApiResponseGenerator.success(authToken, HttpStatus.OK); | ||
public ApiResponse<CustomBody<MemberSignUpResDto>> signUp( | ||
@Valid @RequestBody MemberSignUpReqDto memberSignUpReqDto | ||
) { | ||
MemberSignUpResDto memberSignUpResDto = authService.signUp(memberSignUpReqDto); | ||
return ApiResponseGenerator.success(memberSignUpResDto, HttpStatus.OK); | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
src/main/java/econo/buddybridge/auth/controller/OAuthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package econo.buddybridge.auth.controller; | ||
|
||
import econo.buddybridge.auth.OAuthProvider; | ||
import econo.buddybridge.auth.dto.kakao.KakaoLoginParams; | ||
import econo.buddybridge.auth.exception.AlreadyLogoutException; | ||
import econo.buddybridge.auth.jwt.AuthToken; | ||
import econo.buddybridge.auth.resolver.MemberToken; | ||
import econo.buddybridge.auth.service.OAuthLoginService; | ||
import econo.buddybridge.common.annotation.AllowAnonymous; | ||
import econo.buddybridge.member.dto.MemberResDto; | ||
import econo.buddybridge.utils.api.ApiResponse; | ||
import econo.buddybridge.utils.api.ApiResponse.CustomBody; | ||
import econo.buddybridge.utils.api.ApiResponseGenerator; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpSession; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import java.net.URI; | ||
|
||
@Slf4j | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/oauth") | ||
@Tag(name = "인증 API", description = "인증 관련 API") | ||
public class OAuthController { | ||
|
||
private final OAuthLoginService oAuthLoginService; | ||
|
||
@Value("${oauth.kakao.url.front-url}") | ||
private String frontUrl; | ||
|
||
@Operation(summary = "로그아웃", description = "세션을 제거합니다.") | ||
@AllowAnonymous | ||
@PostMapping("/logout") | ||
public ApiResponse<CustomBody<String>> logout(HttpServletRequest request) { | ||
HttpSession session = request.getSession(false); | ||
if (session != null) { | ||
session.invalidate(); | ||
oAuthLoginService.logout(OAuthProvider.KAKAO); | ||
return ApiResponseGenerator.success("로그아웃 성공", HttpStatus.OK); | ||
} | ||
throw AlreadyLogoutException.EXCEPTION; | ||
} | ||
|
||
@Operation(summary = "카카오 소셜 로그인 (코드로 로그인)", description = "Redirect URL이 백엔드 주소로 설정될 때 사용합니다.") | ||
@GetMapping("/login") | ||
public ApiResponse<CustomBody<MemberResDto>> login(@RequestParam("code") String code, HttpServletRequest request) { | ||
KakaoLoginParams params = new KakaoLoginParams(code); | ||
|
||
MemberResDto memberDto = oAuthLoginService.login(params); | ||
|
||
HttpSession session = request.getSession(true); | ||
session.setAttribute("memberId", memberDto.memberId()); | ||
|
||
// 프론트엔드 주소로 redirect | ||
HttpHeaders httpHeaders = new HttpHeaders(); | ||
httpHeaders.setLocation(URI.create(frontUrl)); | ||
|
||
return ApiResponseGenerator.success(memberDto, httpHeaders, HttpStatus.PERMANENT_REDIRECT); | ||
} | ||
|
||
@Operation(summary = "카카오 소셜 로그인 (토큰으로 로그인)", description = "Redirect URL이 프론트엔드 주소로 설정될 때 사용합니다.") | ||
@PostMapping("/login") | ||
public ApiResponse<CustomBody<MemberResDto>> login(@RequestBody KakaoLoginParams params, HttpServletRequest request) { | ||
MemberResDto memberDto = oAuthLoginService.login(params); | ||
|
||
HttpSession session = request.getSession(true); | ||
session.setAttribute("memberId", memberDto.memberId()); | ||
|
||
return ApiResponseGenerator.success(memberDto, HttpStatus.OK); | ||
} | ||
|
||
// 소셜로그인 with JWT | ||
@Operation(summary = "카카오 소셜 로그인 (JWT)", description = "JWT를 이용하여 로그인합니다.") | ||
@PostMapping("/login/jwt") | ||
public ApiResponse<CustomBody<AuthToken>> loginWithToken(@RequestBody KakaoLoginParams params) { | ||
AuthToken authToken = oAuthLoginService.loginWithToken(params); | ||
return ApiResponseGenerator.success(authToken, HttpStatus.OK); | ||
} | ||
|
||
// refresh token 재발급 | ||
@Operation(summary = "Access Token, Refresh Token 재발급", description = "Refresh Token을 이용하여 두 토큰 모두 재발급합니다.") | ||
@PostMapping("/reissue") | ||
public ApiResponse<CustomBody<AuthToken>> reissue(@MemberToken String token) { | ||
AuthToken authToken = oAuthLoginService.reissue(token); | ||
return ApiResponseGenerator.success(authToken, HttpStatus.OK); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
src/main/java/econo/buddybridge/auth/dto/PasswordHashDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package econo.buddybridge.auth.dto; | ||
|
||
public record PasswordHashDto( | ||
String hashedPassword, | ||
String salt | ||
) { | ||
} |
35 changes: 35 additions & 0 deletions
35
src/main/java/econo/buddybridge/auth/exception/EncoderErrorCode.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package econo.buddybridge.auth.exception; | ||
|
||
import econo.buddybridge.common.exception.ErrorCode; | ||
import org.springframework.http.HttpStatus; | ||
|
||
public enum EncoderErrorCode implements ErrorCode { | ||
ENCRYPT_FAILED("EN001", HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."), | ||
GENERATE_SALT_FAILED("EN002", HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."), | ||
; | ||
|
||
private final String code; | ||
private final HttpStatus httpStatus; | ||
private final String message; | ||
|
||
EncoderErrorCode(String code, HttpStatus httpStatus, String message) { | ||
this.code = code; | ||
this.httpStatus = httpStatus; | ||
this.message = message; | ||
} | ||
|
||
@Override | ||
public String getCode() { | ||
return code; | ||
} | ||
|
||
@Override | ||
public HttpStatus getHttpStatus() { | ||
return httpStatus; | ||
} | ||
|
||
@Override | ||
public String getMessage() { | ||
return message; | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/econo/buddybridge/auth/exception/EncryptFailedException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package econo.buddybridge.auth.exception; | ||
|
||
import econo.buddybridge.common.exception.BusinessException; | ||
|
||
public class EncryptFailedException extends BusinessException { | ||
|
||
public static BusinessException EXCEPTION = new EncryptFailedException(); | ||
|
||
private EncryptFailedException() { | ||
super(EncoderErrorCode.ENCRYPT_FAILED); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/econo/buddybridge/auth/exception/GenerateSaltFailedException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package econo.buddybridge.auth.exception; | ||
|
||
import econo.buddybridge.common.exception.BusinessException; | ||
|
||
public class GenerateSaltFailedException extends BusinessException { | ||
|
||
public static BusinessException EXCEPTION = new GenerateSaltFailedException(); | ||
|
||
private GenerateSaltFailedException() { | ||
super(EncoderErrorCode.GENERATE_SALT_FAILED); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/econo/buddybridge/auth/service/AuthService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package econo.buddybridge.auth.service; | ||
|
||
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; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class AuthService { | ||
|
||
private final MemberService memberService; | ||
|
||
public MemberSignUpResDto signUp(MemberSignUpReqDto memberSignUpReqDto) { | ||
return memberService.createSignUpMember(memberSignUpReqDto); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
src/main/java/econo/buddybridge/auth/utils/PasswordEncoder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package econo.buddybridge.auth.utils; | ||
|
||
import econo.buddybridge.auth.dto.PasswordHashDto; | ||
import econo.buddybridge.auth.exception.EncryptFailedException; | ||
import econo.buddybridge.auth.exception.GenerateSaltFailedException; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.stereotype.Component; | ||
|
||
import javax.crypto.SecretKeyFactory; | ||
import javax.crypto.spec.PBEKeySpec; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.SecureRandom; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.KeySpec; | ||
import java.util.Base64; | ||
|
||
@Slf4j | ||
@Component | ||
public class PasswordEncoder { | ||
|
||
public PasswordHashDto encrypt(String password) { | ||
byte[] salt = generateRandomSalt(); | ||
String hashedPassword = hashPassword(password, salt); | ||
String saltString = Base64.getEncoder().encodeToString(salt); | ||
|
||
return new PasswordHashDto(hashedPassword, saltString); | ||
} | ||
|
||
private String hashPassword(String password, byte[] salt) { | ||
try { | ||
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10000, 128); | ||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); | ||
byte[] hash = factory.generateSecret(spec).getEncoded(); | ||
return Base64.getEncoder().encodeToString(hash); | ||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) { | ||
log.error("비밀번호 암호화에 실패했습니다.", e); // 서버 에러는 로그로 처리 | ||
throw EncryptFailedException.EXCEPTION; | ||
} | ||
} | ||
|
||
private byte[] generateRandomSalt() { | ||
try { | ||
SecureRandom random = new SecureRandom(); | ||
byte[] salt = new byte[16]; | ||
random.nextBytes(salt); | ||
return salt; | ||
} catch (Exception e) { | ||
log.error("솔트 생성에 실패했습니다.", e); // 서버 에러는 로그로 처리 | ||
throw GenerateSaltFailedException.EXCEPTION; | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/econo/buddybridge/member/dto/MemberSignUpReqDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package econo.buddybridge.member.dto; | ||
|
||
import econo.buddybridge.member.entity.Gender; | ||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
import java.time.LocalDate; | ||
|
||
public record MemberSignUpReqDto( | ||
@NotBlank(message = "이름을 입력해주세요.") | ||
String name, | ||
|
||
@NotNull(message = "성별을 선택해주세요.") | ||
Gender gender, | ||
|
||
@NotNull(message = "생년월일을 입력해주세요.") | ||
LocalDate birthDate, | ||
|
||
@NotBlank(message = "이메일을 입력해주세요.") | ||
@Email(message = "이메일 형식이 올바르지 않습니다.") | ||
String email, | ||
|
||
@NotBlank(message = "비밀번호를 입력해주세요.") | ||
String password | ||
) { | ||
|
||
} |
Oops, something went wrong.