-
Notifications
You must be signed in to change notification settings - Fork 4
[feat] 자체 회원가입 처리 구현 #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] 자체 회원가입 처리 구현 #108
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.arom.with_travel.domain.member.controller; | ||
|
|
||
| import com.arom.with_travel.domain.member.dto.request.LocalLoginRequest; | ||
| import com.arom.with_travel.domain.member.dto.request.SignupWithSurveyRequestDto; | ||
| import com.arom.with_travel.domain.member.dto.response.LoginResponse; | ||
| import com.arom.with_travel.domain.member.service.LocalAuthService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/auth") | ||
| @RequiredArgsConstructor | ||
| public class AuthController { | ||
|
|
||
| private final LocalAuthService authService; | ||
|
|
||
| // 이메일 중복 체크 | ||
| @GetMapping("/email-available") | ||
| public boolean emailAvailable(@RequestParam String email) { | ||
| return authService.isEmailAvailable(email); | ||
| } | ||
|
|
||
| // 이메일 등록(회원가입) | ||
| @PostMapping("/register") | ||
| public ResponseEntity<LoginResponse> register(@Valid @RequestBody SignupWithSurveyRequestDto req) { | ||
| return ResponseEntity.ok(authService.registerWithSurvey(req)); | ||
| } | ||
|
|
||
| // 로그인 | ||
| @PostMapping("/login") | ||
| public LoginResponse login(@Valid @RequestBody LocalLoginRequest req) { | ||
| return authService.login(req); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.arom.with_travel.domain.member.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| public class LocalLoginRequest { | ||
| @NotBlank @Email | ||
| private String email; | ||
|
|
||
| @NotBlank @Size(min=8, max=64) | ||
| private String password; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.arom.with_travel.domain.member.dto.response; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public class LoginResponse { | ||
| private String accessToken; | ||
| private String refreshToken; | ||
| private boolean infoChecked; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,100 @@ | ||||||||||||||||||||||||||||||||
| package com.arom.with_travel.domain.member.service; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.Member; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.dto.request.LocalLoginRequest; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.dto.request.MemberSignupRequestDto; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.dto.request.SignupWithSurveyRequestDto; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.dto.response.LoginResponse; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.member.repository.MemberRepository; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.survey.Survey; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.survey.dto.request.SurveyRequestDto; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.domain.survey.repository.SurveyRepository; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.global.exception.BaseException; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.global.exception.error.ErrorCode; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.global.jwt.dto.response.AuthTokenResponse; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.global.security.token.provider.JwtProvider; | ||||||||||||||||||||||||||||||||
| import com.arom.with_travel.global.security.token.service.TokenService; | ||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||
| public class LocalAuthService { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private final MemberRepository memberRepository; | ||||||||||||||||||||||||||||||||
| private final SurveyRepository surveyRepository; | ||||||||||||||||||||||||||||||||
| private final TokenService tokenService; | ||||||||||||||||||||||||||||||||
| private final PasswordEncoder passwordEncoder; | ||||||||||||||||||||||||||||||||
| private final JwtProvider jwtProvider; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||||||||
| public boolean isEmailAvailable(String email) { | ||||||||||||||||||||||||||||||||
| boolean duplicated = memberRepository.existsByEmail(email); | ||||||||||||||||||||||||||||||||
| return !duplicated; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 신규 회원 추가 정보 + 설문 통합 등록 | ||||||||||||||||||||||||||||||||
| public LoginResponse registerWithSurvey(SignupWithSurveyRequestDto req) { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| String email = req.getExtraInfo().getEmail(); | ||||||||||||||||||||||||||||||||
| if(!isEmailAvailable(email)) { | ||||||||||||||||||||||||||||||||
| throw BaseException.from(ErrorCode.DUPLICATED_EMAIL); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| MemberSignupRequestDto extra = req.getExtraInfo(); | ||||||||||||||||||||||||||||||||
| String encodedPassword = passwordEncoder.encode(extra.getPassword()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Member member = Member.builder() | ||||||||||||||||||||||||||||||||
| .email(extra.getEmail()) | ||||||||||||||||||||||||||||||||
| .password(encodedPassword) | ||||||||||||||||||||||||||||||||
| .name(extra.getName()) | ||||||||||||||||||||||||||||||||
| .phone(extra.getPhone()) | ||||||||||||||||||||||||||||||||
| .birth(extra.getBirthdate()) | ||||||||||||||||||||||||||||||||
| .gender(extra.getGender()) | ||||||||||||||||||||||||||||||||
| .nickname(extra.getNickname()) | ||||||||||||||||||||||||||||||||
| .introduction(extra.getIntroduction()) | ||||||||||||||||||||||||||||||||
| .role(Member.Role.USER) | ||||||||||||||||||||||||||||||||
| .additionalDataChecked(false) | ||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| member = memberRepository.save(member); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| SurveyRequestDto s = req.getSurvey(); | ||||||||||||||||||||||||||||||||
| Survey survey = Survey.create(member, s); | ||||||||||||||||||||||||||||||||
| surveyRepository.save(survey); | ||||||||||||||||||||||||||||||||
| member.setSurvey(survey); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| member.markAdditionalDataChecked(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| AuthTokenResponse tokenPair = tokenService.issueTokenPair(member.getEmail()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return new LoginResponse( | ||||||||||||||||||||||||||||||||
| tokenPair.getAccessToken(), | ||||||||||||||||||||||||||||||||
| tokenPair.getRefreshToken(), | ||||||||||||||||||||||||||||||||
| member.getAdditionalDataChecked() | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private Member getUserByLoginEmailOrElseThrow(String loginEmail) { | ||||||||||||||||||||||||||||||||
| return memberRepository.findByEmail(loginEmail) | ||||||||||||||||||||||||||||||||
| .orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND)); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 로그인: raw, hashed 비번 비교 → 토큰 발급 | ||||||||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||||||||
| public LoginResponse login(LocalLoginRequest req) { | ||||||||||||||||||||||||||||||||
| Member m = memberRepository.findByEmail(req.getEmail()) | ||||||||||||||||||||||||||||||||
| .orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND)); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (m.getPassword() == null || !passwordEncoder.matches(req.getPassword(), m.getPassword())) { | ||||||||||||||||||||||||||||||||
|
Comment on lines
89
to
92
|
||||||||||||||||||||||||||||||||
| Member m = memberRepository.findByEmail(req.getEmail()) | |
| .orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND)); | |
| if (m.getPassword() == null || !passwordEncoder.matches(req.getPassword(), m.getPassword())) { | |
| Member m = memberRepository.findByEmail(req.getEmail()).orElse(null); | |
| if (m == null || m.getPassword() == null || !passwordEncoder.matches(req.getPassword(), m.getPassword())) { |
Copilot
AI
Aug 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그인 실패 시 MEMBER_NOT_FOUND와 INVALID_CREDENTIALS 두 가지 다른 에러 코드를 사용하면 공격자가 유효한 이메일 주소를 식별할 수 있습니다. 모든 로그인 실패 케이스에 대해 동일한 에러 코드를 사용하는 것을 권장합니다.
| Member m = memberRepository.findByEmail(req.getEmail()) | |
| .orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND)); | |
| if (m.getPassword() == null || !passwordEncoder.matches(req.getPassword(), m.getPassword())) { | |
| // Use a dummy password hash to prevent timing attacks and user enumeration | |
| final String DUMMY_PASSWORD_HASH = "$2a$10$7EqJtq98hPqEX7fNZaFWoOa5g5r9Z3rro3yd1y0T5k4bFZ5WD7FZm"; // bcrypt for "dummy_password" | |
| Member m = memberRepository.findByEmail(req.getEmail()).orElse(null); | |
| String passwordHash = (m != null && m.getPassword() != null) ? m.getPassword() : DUMMY_PASSWORD_HASH; | |
| if (m == null || !passwordEncoder.matches(req.getPassword(), passwordHash)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateExtraInfo 메서드가 이제 기본 정보(email, password, name, phone)까지 업데이트하므로 메서드명이 부적절합니다. updateMemberInfo 또는 updateAllInfo와 같이 더 명확한 이름으로 변경하는 것을 권장합니다.