Skip to content

Commit dccc179

Browse files
committed
[FIX] 소셜 로그인 시, 유저 정보 요청을 직접 하기 위한 변경사항
1 parent bcc2c03 commit dccc179

File tree

5 files changed

+132
-27
lines changed

5 files changed

+132
-27
lines changed

src/main/java/com/example/ai_tutor/domain/auth/application/AuthService.java

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
package com.example.ai_tutor.domain.auth.application;
22

33
import com.example.ai_tutor.domain.auth.domain.Token;
4-
import com.example.ai_tutor.domain.auth.dto.SignInReq;
4+
import com.example.ai_tutor.domain.auth.dto.*;
55
import com.example.ai_tutor.domain.auth.exception.InvalidTokenException;
66
import com.example.ai_tutor.domain.auth.domain.repository.TokenRepository;
7-
import com.example.ai_tutor.domain.auth.dto.AuthRes;
8-
import com.example.ai_tutor.domain.auth.dto.RefreshTokenReq;
9-
import com.example.ai_tutor.domain.auth.dto.TokenMapping;
107
import com.example.ai_tutor.domain.professor.domain.repository.ProfessorRepository;
118
import com.example.ai_tutor.domain.user.domain.Provider;
129
import com.example.ai_tutor.domain.user.domain.User;
1310
import com.example.ai_tutor.domain.user.domain.repository.UserRepository;
1411
import com.example.ai_tutor.global.DefaultAssert;
1512
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
16-
import com.example.ai_tutor.global.error.DefaultException;
1713
import com.example.ai_tutor.global.payload.ApiResponse;
18-
import com.example.ai_tutor.global.payload.ErrorCode;
1914
import com.example.ai_tutor.global.payload.Message;
2015
import lombok.RequiredArgsConstructor;
2116
import org.springframework.http.ResponseEntity;
2217
import org.springframework.security.authentication.AuthenticationManager;
2318
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
2419
import org.springframework.security.core.Authentication;
2520
import org.springframework.security.core.context.SecurityContextHolder;
21+
import org.springframework.security.core.userdetails.UserDetails;
22+
import org.springframework.security.core.userdetails.UserDetailsService;
2623
import org.springframework.stereotype.Service;
2724
import org.springframework.transaction.annotation.Transactional;
25+
import org.springframework.web.bind.annotation.RequestHeader;
2826

2927
import java.util.Optional;
3028

@@ -35,6 +33,8 @@ public class AuthService {
3533

3634
private final CustomTokenProviderService customTokenProviderService;
3735
private final AuthenticationManager authenticationManager;
36+
private final IdTokenVerifier idTokenVerifier;
37+
private final UserDetailsService userDetailsService;
3838

3939
private final TokenRepository tokenRepository;
4040
private final UserRepository userRepository;
@@ -108,33 +108,46 @@ private boolean valid(String refreshToken){
108108
return true;
109109
}
110110

111+
111112
@Transactional
112-
public ResponseEntity<?> signIn(SignInReq signInReq) {
113-
String email = signInReq.getEmail();
114-
// 유저 조회
115-
User user = userRepository.findByEmail(email)
116-
.orElseThrow(() -> new DefaultException(ErrorCode.INVALID_CHECK, "유저 정보가 유효하지 않습니다."));
113+
public ResponseEntity<?> signIn(SignInReq signInReq, @RequestHeader("Authorization") String authorizationHeader) {
114+
// 1. 토큰 파싱
115+
String googleAccessToken = authorizationHeader.replace("Bearer ", "").trim();
116+
117+
UserInfo userInfo = idTokenVerifier.verifyIdToken(googleAccessToken, signInReq.getEmail());
117118

119+
// 2. ID 토큰 검증 및 사용자 정보 추출
120+
if (userInfo == null) {
121+
throw new RuntimeException("유효하지 않은 ID 토큰");
122+
}
123+
User user = userRepository.findByEmail(userInfo.getEmail())
124+
.orElseGet(() -> {
125+
User newUser = User.builder()
126+
.email(userInfo.getEmail())
127+
.name(userInfo.getName())
128+
.password("oauth-only")
129+
.provider(Provider.google)
130+
.providerId(signInReq.getProviderId())
131+
.build();
132+
return userRepository.save(newUser);
133+
});
134+
135+
// 4. Spring Security 인증 객체 생성
118136
// 인증 객체 생성 및 SecurityContext 등록
119-
Authentication authentication = authenticationManager.authenticate(
120-
new UsernamePasswordAuthenticationToken(
121-
signInReq.getEmail(),
122-
signInReq.getProviderId()
123-
)
124-
);
137+
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getEmail());
138+
UsernamePasswordAuthenticationToken authentication =
139+
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
125140
SecurityContextHolder.getContext().setAuthentication(authentication);
126-
127-
// JWT 토큰 생성
141+
// 5. JWT 토큰 생성 및 refresh 저장
128142
TokenMapping tokenMapping = customTokenProviderService.createToken(authentication);
129143

130-
// 리프레시 토큰 DB 저장
131144
Token token = Token.builder()
145+
.userEmail(user.getEmail())
132146
.refreshToken(tokenMapping.getRefreshToken())
133-
.userEmail(tokenMapping.getEmail())
134147
.build();
135148
tokenRepository.save(token);
136149

137-
// 응답 DTO 생성
150+
// 6. 응답 구성
138151
AuthRes authResponse = AuthRes.builder()
139152
.accessToken(tokenMapping.getAccessToken())
140153
.refreshToken(token.getRefreshToken())
@@ -148,4 +161,7 @@ public ResponseEntity<?> signIn(SignInReq signInReq) {
148161
return ResponseEntity.ok(apiResponse);
149162
}
150163

164+
165+
166+
151167
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.example.ai_tutor.domain.auth.application;
2+
3+
import com.example.ai_tutor.domain.auth.dto.UserInfo;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.http.*;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.client.RestTemplate;
12+
13+
@Slf4j
14+
@Component
15+
@RequiredArgsConstructor
16+
public class IdTokenVerifier {
17+
18+
@Value("${spring.security.oauth2.client.provider.google.user-info-uri}")
19+
private String googleUserInfoUri;
20+
21+
@Value("${app.auth.token-secret}")
22+
private String jwtSecret;
23+
24+
private final ObjectMapper objectMapper;
25+
private final RestTemplate restTemplate;
26+
27+
public UserInfo verifyIdToken(String idToken, String email) {
28+
try {
29+
HttpHeaders headers = new HttpHeaders();
30+
headers.set("Authorization", "Bearer " + idToken);
31+
HttpEntity<String> entity = new HttpEntity<>(headers);
32+
33+
ResponseEntity<String> response = restTemplate.postForEntity(googleUserInfoUri, entity, String.class);
34+
35+
if (response.getStatusCode().is2xxSuccessful()) {
36+
JsonNode userInfo = objectMapper.readTree(response.getBody());
37+
38+
String tokenEmail = userInfo.path("email").asText();
39+
String name = userInfo.path("name").asText(); // 여기!
40+
41+
if (!email.equals(tokenEmail)) {
42+
throw new IllegalArgumentException("ID 토큰의 이메일과 요청된 이메일이 일치하지 않습니다.");
43+
}
44+
45+
return new UserInfo(tokenEmail, name); // DTO에 담아 반환
46+
} else {
47+
throw new RuntimeException("ID 토큰 검증 실패: " + response.getStatusCode());
48+
}
49+
} catch (Exception e) {
50+
throw new RuntimeException("ID 토큰 검증 중 오류 발생", e);
51+
}
52+
}
53+
54+
55+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.ai_tutor.domain.auth.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class UserInfo {
9+
private String email;
10+
private String name;
11+
}

src/main/java/com/example/ai_tutor/domain/auth/presentation/AuthController.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,25 @@ public class AuthController {
3131

3232
@Operation(summary = "로그인", description = "사용자가 로그인을 수행합니다.")
3333
@ApiResponses(value = {
34-
@ApiResponse(responseCode = "200", description = "로그인 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class) ) } ),
35-
@ApiResponse(responseCode = "400", description = "로그인 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ),
34+
@ApiResponse(responseCode = "200", description = "로그인 성공", content = {
35+
@Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class))
36+
}),
37+
@ApiResponse(responseCode = "400", description = "로그인 실패", content = {
38+
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
39+
}),
3640
})
37-
@PostMapping(value="/sign-in")
41+
@PostMapping(value = "/sign-in")
3842
public ResponseEntity<?> signIn(
39-
@Parameter(description = "SignInReq Schema를 확인해주세요.", required = true) @RequestBody SignInReq signInReq
43+
@Parameter(description = "SignInReq Schema를 확인해주세요.", required = true)
44+
@RequestBody SignInReq signInReq,
45+
46+
@Parameter(hidden = true) // Swagger 문서에 안 보이게
47+
@RequestHeader("Authorization") String authorizationHeader
4048
) {
41-
return authService.signIn(signInReq);
49+
return authService.signIn(signInReq, authorizationHeader);
4250
}
4351

52+
4453
@Operation(summary = "토큰 갱신", description = "신규 토큰 갱신을 수행합니다.")
4554
@ApiResponses(value = {
4655
@ApiResponse(responseCode = "200", description = "토큰 갱신 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = AuthRes.class) ) } ),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.example.ai_tutor.global.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.client.RestTemplate;
6+
7+
@Configuration
8+
public class AppConfig {
9+
10+
@Bean
11+
public RestTemplate restTemplate() {
12+
return new RestTemplate();
13+
}
14+
}

0 commit comments

Comments
 (0)