-
Notifications
You must be signed in to change notification settings - Fork 0
feat : google login 구현 완료 (ios 구현 중) #30
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
Changes from all commits
2f9b3f0
b0115dd
a258e51
4e769a7
0cd921f
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,7 @@ | ||
| package ita.tinybite.domain.auth.dto.request; | ||
|
|
||
| import ita.tinybite.domain.user.constant.PlatformType; | ||
|
|
||
| public record GoogleAndAppleLoginReq(String idToken, | ||
| PlatformType platformType) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package ita.tinybite.domain.auth.dto.request; | ||
|
|
||
| import ita.tinybite.domain.user.constant.PlatformType; | ||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record GoogleAndAppleSignupRequest( | ||
| @NotBlank(message = "idToken은 필수입니다") | ||
| String idToken, | ||
| @NotBlank(message = "전화번호는 필수입니다") | ||
| String phone, | ||
| @NotBlank(message = "닉네임은 필수입니다") | ||
| String nickname, | ||
| @NotBlank(message = "위치 정보 필수입니다") | ||
| String location, | ||
| @NotBlank(message = "플랫폼정보는 필수입니다") | ||
| PlatformType platform | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| package ita.tinybite.domain.auth.service; | ||
|
|
||
| import ita.tinybite.domain.auth.dto.request.KakaoLoginRequest; | ||
| import ita.tinybite.domain.auth.dto.request.KakaoSignupRequest; | ||
| import ita.tinybite.domain.auth.dto.request.RefreshTokenRequest; | ||
| import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; | ||
| import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; | ||
| import com.google.api.client.http.javanet.NetHttpTransport; | ||
| import com.google.api.client.json.gson.GsonFactory; | ||
| import ita.tinybite.domain.auth.dto.request.*; | ||
| import ita.tinybite.domain.auth.dto.response.AuthResponse; | ||
| import ita.tinybite.domain.auth.dto.response.UserDto; | ||
| import ita.tinybite.domain.auth.entity.JwtTokenProvider; | ||
|
|
@@ -11,17 +13,27 @@ | |
| import ita.tinybite.domain.auth.kakao.KakaoApiClient.KakaoUserInfo; | ||
| import ita.tinybite.domain.auth.repository.RefreshTokenRepository; | ||
| import ita.tinybite.domain.user.constant.LoginType; | ||
| import ita.tinybite.domain.user.constant.PlatformType; | ||
| import ita.tinybite.domain.user.constant.UserStatus; | ||
| import ita.tinybite.domain.user.entity.User; | ||
| import ita.tinybite.domain.user.repository.UserRepository; | ||
| import ita.tinybite.global.exception.BusinessException; | ||
| import ita.tinybite.global.exception.errorcode.AuthErrorCode; | ||
| import ita.tinybite.global.exception.errorcode.UserErrorCode; | ||
| import ita.tinybite.global.util.NicknameGenerator; | ||
| import jakarta.transaction.Transactional; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.security.oauth2.jwt.JwtDecoder; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.io.*; | ||
| import java.security.GeneralSecurityException; | ||
| import java.time.LocalDateTime; | ||
| import java.util.Collections; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
|
|
@@ -32,8 +44,18 @@ public class AuthService { | |
| private final RefreshTokenRepository refreshTokenRepository; | ||
| private final KakaoApiClient kakaoApiClient; | ||
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final JwtDecoder appleJwtDecoder; | ||
| private final NicknameGenerator nicknameGenerator; | ||
|
|
||
| // @Value("${apple.client-id}") | ||
| // private String appleClientId; | ||
|
|
||
| @Value("${google.android-id}") | ||
| private String googleAndroidId; | ||
|
|
||
| @Value("${google.ios-id}") | ||
| private String googleIosId; | ||
|
|
||
| @Transactional | ||
| public AuthResponse kakaoSignup(KakaoSignupRequest request) { | ||
| // 카카오 API로 유저 정보 조회 | ||
|
|
@@ -109,6 +131,129 @@ public AuthResponse kakaoLogin(KakaoLoginRequest request) { | |
| .build(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AuthResponse googleSignup(@Valid GoogleAndAppleSignupRequest req) { | ||
| // idToken으로 이메일 추출 | ||
| String email = getEmailFromIdToken(req.idToken(), req.platform(), LoginType.GOOGLE); | ||
|
|
||
| // 해당 이메일의 유저 find | ||
| User user = userRepository.findByEmail(email) | ||
| .orElseThrow(() -> BusinessException.of(UserErrorCode.USER_NOT_EXISTS)); | ||
|
|
||
| // req필드로 유저 필드 업데이트 -> 실질적 회원가입 | ||
| user.updateSignupInfo(req, email); | ||
| userRepository.save(user); | ||
|
|
||
| return getAuthResponse(user); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AuthResponse googleLogin(@Valid GoogleAndAppleLoginReq req) { | ||
| // idToken으로 이메일 추출 | ||
| String email = getEmailFromIdToken(req.idToken(), req.platformType(), LoginType.GOOGLE); | ||
| // 해당 이메일로 유저 찾은 후 응답 반환 (accessToken, refreshToken) | ||
| return getUser(email); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AuthResponse appleSignup(@Valid GoogleAndAppleSignupRequest req) { | ||
| String email = getEmailFromIdToken(req.idToken(), req.platform(), LoginType.APPLE); | ||
|
|
||
| // 해당 이메일의 유저 find | ||
| User user = userRepository.findByEmail(email) | ||
| .orElseThrow(() -> BusinessException.of(UserErrorCode.USER_NOT_EXISTS)); | ||
|
|
||
| // req필드로 유저 필드 업데이트 -> 실질적 회원가입 | ||
| user.updateSignupInfo(req, email); | ||
| userRepository.save(user); | ||
|
|
||
| return getAuthResponse(user); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AuthResponse appleLogin(@Valid GoogleAndAppleLoginReq req) { | ||
| // idToken으로 이메일 추출 | ||
| String email = getEmailFromIdToken(req.idToken(), req.platformType(), LoginType.APPLE); | ||
| // 해당 이메일로 유저 찾은 후 응답 반환 (AuthResponse) | ||
| return getUser(email); | ||
| } | ||
|
|
||
| private AuthResponse getAuthResponse(User user) { | ||
| // 4. JWT 토큰 생성 | ||
| String accessToken = jwtTokenProvider.generateAccessToken(user); | ||
| String refreshToken = jwtTokenProvider.generateRefreshToken(user); | ||
|
|
||
| // 5. 기존 Refresh Token 삭제 후 새로 저장 | ||
| refreshTokenRepository.deleteByUserId(user.getUserId()); | ||
| saveRefreshToken(user.getUserId(), refreshToken); | ||
|
|
||
| log.info("로그인 성공 - User ID: {}, Email: {}", user.getUserId(), user.getEmail()); | ||
|
|
||
| // 6. 응답 생성 | ||
| return AuthResponse.builder() | ||
| .accessToken(accessToken) | ||
| .refreshToken(refreshToken) | ||
| .tokenType("Bearer") | ||
| .expiresIn(3600L) | ||
| .user(UserDto.from(user)) | ||
| .build(); | ||
| } | ||
|
|
||
| // 구글과 애플 통합 | ||
| private String getEmailFromIdToken(String idToken, PlatformType platformType, LoginType loginType) { | ||
| switch(loginType) { | ||
| case GOOGLE -> { | ||
|
|
||
| String clientId = switch (platformType) { | ||
| case ANDROID -> googleAndroidId; | ||
| case IOS -> googleIosId; | ||
| }; | ||
|
|
||
| try { | ||
| GoogleIdTokenVerifier googleVerifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory()) | ||
| .setAudience(Collections.singletonList(clientId)) | ||
| .build(); | ||
|
|
||
| GoogleIdToken token = googleVerifier.verify(idToken); | ||
|
|
||
| if(token == null) { | ||
| throw BusinessException.of(AuthErrorCode.INVALID_TOKEN); | ||
| } | ||
|
|
||
| return token.getPayload().getEmail(); | ||
|
|
||
| } catch (GeneralSecurityException | IOException e) { | ||
| throw BusinessException.of(AuthErrorCode.GOOGLE_LOGIN_ERROR); | ||
| } | ||
| } | ||
| case APPLE -> { | ||
| //TODO Apple 구현 예정 | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
Comment on lines
+202
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apple 로그인 미구현으로 인한 NPE 위험
Apple 구현 전까지 명시적 예외를 던지도록 수정하세요: case APPLE -> {
- //TODO Apple 구현 예정
+ // TODO Apple 구현 예정
+ throw BusinessException.of(AuthErrorCode.APPLE_LOGIN_ERROR);
}
}
- return null;
+ throw new IllegalStateException("Unsupported login type: " + loginType);
}또한, |
||
|
|
||
| private AuthResponse getUser(String email) { | ||
| User user = userRepository.findByEmail(email) | ||
| .orElseThrow(() -> { | ||
| // 이메일로 가입된 유저가 없을 시, INACTIVE로 임시 생성 | ||
| // 회원가입 시 해당 임시 유저를 통해 마저 회원가입 진행 | ||
| userRepository.save(User.builder() | ||
| .email(email) | ||
| .status(UserStatus.INACTIVE) | ||
| .build()); | ||
|
|
||
| return BusinessException.of(UserErrorCode.USER_NOT_EXISTS); | ||
| }); | ||
|
|
||
| // 3. 탈퇴한 사용자 체크 | ||
| if (user.getStatus() == UserStatus.WITHDRAW) { | ||
| throw new RuntimeException("탈퇴한 사용자입니다."); | ||
| } | ||
|
|
||
| return getAuthResponse(user); | ||
| } | ||
|
Comment on lines
+236
to
+255
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat src/main/java/ita/tinybite/domain/auth/service/AuthService.java | head -300 | tail -100Repository: tinybite-2025/tinybite-server Length of output: 3713 🏁 Script executed: rg "UserErrorCode" -A 2 -B 2 --max-count=20Repository: tinybite-2025/tinybite-server Length of output: 4439 🏁 Script executed: fd "User.java" -o java/entity -o java/domainRepository: tinybite-2025/tinybite-server Length of output: 214 🏁 Script executed: rg "class User" -A 10 --type javaRepository: tinybite-2025/tinybite-server Length of output: 5198 🏁 Script executed: rg "enum UserErrorCode" -A 30Repository: tinybite-2025/tinybite-server Length of output: 1661 🏁 Script executed: fd "User.java" -type f | head -5Repository: tinybite-2025/tinybite-server Length of output: 243 🏁 Script executed: rg "class User" -A 50 --type javaRepository: tinybite-2025/tinybite-server Length of output: 18490 🏁 Script executed: rg "updateSignupInfo" -B 5 -A 20Repository: tinybite-2025/tinybite-server Length of output: 6253 🏁 Script executed: rg "LoginType" --type java -B 2 -A 2Repository: tinybite-2025/tinybite-server Length of output: 7825 🏁 Script executed: rg "USER_WITHDRAWN\|WITHDRAW" --type java -B 2 -A 2 | head -40Repository: tinybite-2025/tinybite-server Length of output: 55 🏁 Script executed: cat src/main/java/ita/tinybite/global/exception/errorcode/UserErrorCode.javaRepository: tinybite-2025/tinybite-server Length of output: 618 일관성 없는 예외 처리 - 에러 코드 누락 Line 251에서 현재 // UserErrorCode.java
public enum UserErrorCode implements ErrorCode {
USER_NOT_EXISTS(HttpStatus.NOT_FOUND, "USER_NOT_EXISTS", "존재하지 않는 유저입니다."),
+ USER_WITHDRAWN(HttpStatus.FORBIDDEN, "USER_WITHDRAWN", "탈퇴한 사용자입니다."),
;// AuthService.java
if (user.getStatus() == UserStatus.WITHDRAW) {
- throw new RuntimeException("탈퇴한 사용자입니다.");
+ throw BusinessException.of(UserErrorCode.USER_WITHDRAWN);
}또한
🤖 Prompt for AI Agents |
||
|
|
||
| @Transactional | ||
| public AuthResponse refreshToken(RefreshTokenRequest request) { | ||
| String refreshTokenValue = request.getRefreshToken(); | ||
|
|
@@ -148,6 +293,7 @@ public AuthResponse refreshToken(RefreshTokenRequest request) { | |
| @Transactional | ||
| public void logout(Long userId) { | ||
| refreshTokenRepository.deleteByUserId(userId); | ||
| userRepository.deleteById(userId); | ||
| log.info("로그아웃 - User ID: {}", userId); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| package ita.tinybite.domain.user.constant; | ||
|
|
||
| public enum LoginType { | ||
| KAKAO, GOOGLE | ||
| KAKAO, GOOGLE, APPLE | ||
|
|
||
| ; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package ita.tinybite.domain.user.constant; | ||
|
|
||
| public enum PlatformType { | ||
| ANDROID, | ||
| IOS, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package ita.tinybite.global.config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.security.oauth2.jwt.JwtDecoder; | ||
| import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; | ||
|
|
||
| @Configuration | ||
| public class AppleConfig { | ||
|
|
||
| private static final String APPLE_JWK_URL = "https://appleid.apple.com/auth/keys"; | ||
|
|
||
| @Bean | ||
| public JwtDecoder appleJwtDecoder() { | ||
| return NimbusJwtDecoder | ||
| .withJwkSetUri(APPLE_JWK_URL) | ||
| .build(); | ||
| } | ||
| } |
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.
개인정보(이메일) 로깅 - 컴플라이언스 위험
이메일 주소는 개인식별정보(PII)로, 로그에 기록하면 GDPR/CCPA 등 개인정보보호 규정 위반 가능성이 있습니다. User ID만으로 디버깅에 충분합니다.
Line 122에도 동일한 패턴이 있으니 함께 수정해 주세요.
📝 Committable suggestion
🤖 Prompt for AI Agents