-
Notifications
You must be signed in to change notification settings - Fork 0
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: 카카오 로그인 구현 #60
feat: 카카오 로그인 구현 #60
Conversation
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.
고생하셨습니다!!!!!
서비스단 테스트는 아마 프라이빗 메서드 제외하고 로그인 부분만 테스트 코드 작성하심 될거에요!
return null; | ||
} | ||
|
||
private void sendErrorResponse(HttpServletResponse response, String message, String code) throws IOException { |
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.
P3
파라미터로 하나씩 작성하는 것 보다 ErrorCode 자체를 받아서 생성하도록 만드시는게 좋을 것 같아요.
errorResponse.put("statusCode", 401); | ||
errorResponse.put("code", code); | ||
errorResponse.put("message", message); |
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.
P3
에러코드로 받는다면 ErrorCode.getStatusCode, ErrorCode.getCode, ErrorCode.getMessage 형식으로 쓸 수 있어서
뒤에 코드에서 하나씩 string 으로 작성하지 않으셔도 돼요!
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.
이부분에서 계속 예외처리가 안되더라구요.. 일단은 에러코드 형식에 맞게 저렇게 써놓은 상태입니다.
https://velog.io/@jsang_log/Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-JWT
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.
아 저는 커스텀 에러를 던지라는게 아니라 저 블로그 방식처럼 파라미터로 ErrorCode 를 받아서 메서드로 처리하라는 의미였어요
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.
블로그 코드보면
public static void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException { // 인자로 ErrorCode를 받음
response.setContentType("application/json;charset=UTF-8");
// 내용을 ErrorCode의 메서드 활용해서 작성 response.setStatus(errorCode.getHttpStatus().value());
ObjectMapper objectMapper = new ObjectMapper();
ErrorResponse errorResponse = new ErrorResponse
(errorCode, errorCode.getMessage());
import java.util.Set; | ||
|
||
@RequiredArgsConstructor | ||
@Service |
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.
P4
@Component
로 등록해도 되지 않나요? @Service
를 사용하신 이유가 무엇일까요?
System.out.println("Token issued at: " + now); | ||
System.out.println("Token expires at: " + expiry); |
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.
P2
표준 출력보다는 로그 출력을 사용하시는걸 권장드려요! (성능상으로 이슈가 큰 부분이에요)
Lombok의 @Slf4j
import 하시고
log.warn()"Token issued at: {}", now);
형식으로 작성하시면 돼요!
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 typ(타입) : JWT | ||
.setIssuedAt(now) // 내용 iat(발급 일시) : 현재 시간 | ||
.setExpiration(expiry) // 내용 exp(만료일시) : expiry 멤버 변수값 | ||
.setSubject(String.valueOf(user.getId())) // 내용 sub(토큰 제목) : 회원 ID | ||
.claim("id", user.getId()) // 클레임 id : 회원 ID | ||
.claim("category", category) // access or refresh | ||
.signWith(getSecretKey(), SignatureAlgorithm.HS256) // HS256 방식으로 암호화 |
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.
P4
이곳에 있는 주석은 지워주셔도 될 것 같아요ㅎㅎ
*/ | ||
public boolean isExpired(String token) { | ||
Claims claims = getClaims(token); | ||
return claims.getExpiration().before(new Date()); |
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.
P4
현재 프로젝트에서 Clock
을 사용하고 있기 때문에
private final Clock clock;
컴포넌트 상단에서 Clock 을 의존성으로 받아주시고
new LocalDateTime(Clock)
으로 설정하셔야 테스트에서.... 문제가 안생깁니다 ㅎㅎ....
*/ | ||
public String getCategory(String token) { | ||
Claims claims = getClaims(token); | ||
return claims.get("category", String.class); |
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.
P5
카테고리는 어떤 의미인가요?
boolean existsByProviderId(Long providerId); | ||
|
||
OauthUser findByProviderId(Long providerId); |
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.
P4
Optional 로 감싸주시는게 좋을 것 같습니다!
return UserResponse.loginDTO.builder() | ||
.oauthId(oauthId) | ||
.accessToken(accessToken) | ||
.refreshToken(refreshToken) | ||
.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.
P3
accessToken
과 refreshToken
이 null 인 경우에 validation 이 필요해 보입니다!
try { | ||
jsonNode = objectMapper.readTree(responseBody); | ||
} catch (JsonProcessingException e) { | ||
throw new RuntimeException(e); |
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.
P4
클라이언트에게 정의된 메시지가 갈 수 있게 CustomException 의 서버 에러로 던지는게 좋을 것 같습니다!
HttpServletResponse response = mock(HttpServletResponse.class); | ||
|
||
// Mocking repository and token provider | ||
when(oauthUserRepository.findByProviderId(anyLong())).thenThrow(new CustomException(ErrorCode.INVALID_CODE)); |
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.
인가코드 유효성 검증은
oauthUserRespository
가 아니라 UserService
의 getAccessToken
에서 일어나고 있습니다.
따라서 이 부분은 불필요한 stubbing 입니다.
또한 userSerivce
는 mock 객체가 아니므로, 등록해둔 부분을 제외하면 실제 클래스의 메서드로 동작하고 있습니다.
따라서 유효성 검증이 일어나는 부분을 mock 처리해주셔야 합니다.
현재 유효성 검증은 http request를 보내는 RestTemplate
에서 일어나고 있으므로
RestTemplate
을 @Mock
으로 등록해주시고,
@BeforeEach
를 다음과 같이 설정한 후
@BeforeEach
public void init() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
mockServer.expect(ExpectedCount.once(),
requestTo(new URI("https://kauth.kakao.com/oauth/token")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
);
위와 같이 모킹해주시면 됩니다.
when(oauthUserRepository.findByProviderId(anyLong())).thenReturn(null); | ||
when(tokenProvider.generateToken(any(User.class), any(), any())) | ||
.thenReturn(accessToken) // 첫 번째 호출 반환값 | ||
.thenReturn(refreshToken); // 두 번째 호출 반환값 |
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.
현재 코드를 확인해보면
Optional<OauthUser> optionalUser = oauthUserRepository.findByProviderId(oauthId);
//DB에 회원정보가 있을때 토큰 발급
if (optionalUser.isPresent()) {
OauthUser user = optionalUser.get();
//토큰 생성
accessToken = tokenProvider.generateToken(user, Duration.ofHours(2), "access");
refreshToken = tokenProvider.generateToken(user, Duration.ofDays(14), "refresh");
log.info("access: " + accessToken);
log.info("refresh : " + refreshToken);
//Refresh 토큰 저장
redisUtils.setData(user.getId().toString(), refreshToken, Duration.ofDays(14).toMillis());
}
//응답 설정
return UserResponse.loginDTO.builder()
.oauthId(oauthId)
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
신규 사용자인 경우 (findByProviderId
의 결과가 null 인 경우)
accessToken
과 refreshToken
이 발급되고 있지 않습니다.
신규 사용자면 해당 토큰 값들에 null 이 들어가게 되어 로그인이 동작하지 않을 것 같아요.
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.
우선 유저 테스트는 제거해주시고,
build.gradle
에 jacoco 에서 user 도메인은 제외해주세요.
그리고 코멘트에 대한 답장 부탁드립니다.
|
||
public String generateToken(User user, Duration expiredAt, String category) { | ||
LocalDateTime now = LocalDateTime.now(clock); | ||
Date expiryDate = Date.from(now.plus(expiredAt).atZone(ZoneId.systemDefault()).toInstant()); |
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.
참고로 Date
는 거의 모든 새로운 프로젝트에서 더 이상 권장되지 않습니다.
Jwt
라이브러리 때문에 Date
를 쓰신 것 같지만, 참고용으로 자료 올려드립니다.
.build() | ||
.parseClaimsJws(token); | ||
return true; | ||
} catch (ExpiredJwtException e) { |
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.
이 부분 로컬에서 토큰이 유효하지 않을 때, 에러가 발생하는지 확인해주세요.
|
||
@PostMapping("/login") | ||
@Operation(summary = "카카오 로그인", description = "카카오 인가코드를 통해 로그인 후 JWT 토큰을 생성합니다.") | ||
public ResponseEntity<BasicResponse<UserResponse.loginDTO>> kakaoLogin(@RequestHeader String code, HttpServletRequest request, HttpServletResponse response) { |
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.
request
와 response
는 사용되고 있지않습니다.
서비스에서도 마찬가지고요.
이미 헤더에 있는 인가 코드 @RequestHeader
로 넘기고 있습니다.
@PostMapping("/login") | ||
@Operation(summary = "카카오 로그인", description = "카카오 인가코드를 통해 로그인 후 JWT 토큰을 생성합니다.") | ||
public ResponseEntity<BasicResponse<UserResponse.loginDTO>> kakaoLogin(@RequestHeader String code, HttpServletRequest request, HttpServletResponse response) { | ||
UserResponse.loginDTO loginResponse = userService.kakaoLogin(code, request, response); |
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.
사용하지 않는 인수는 제거해주세요.
|
||
@PostMapping("/login") | ||
@Operation(summary = "카카오 로그인", description = "카카오 인가코드를 통해 로그인 후 JWT 토큰을 생성합니다.") | ||
public ResponseEntity<BasicResponse<UserResponse.loginDTO>> kakaoLogin(@RequestHeader String code, HttpServletRequest request, HttpServletResponse response) { |
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.
컨트롤러 메서드이기 때문에 파라미터에 대한 검증이 이루어져야 합니다.
code 에 대해 @NotNull
과 같은 validation 을 추가해주세요.
private String REDIRECT_URI; | ||
|
||
@Transactional | ||
public UserResponse.loginDTO kakaoLogin(String code, HttpServletRequest request, HttpServletResponse response) { |
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.
HttpServletRequest request, HttpServletResponse response
는 사용하지 않는 파라미터 입니다.
if (optionalUser.isPresent()) { | ||
OauthUser user = optionalUser.get(); | ||
|
||
//토큰 생성 | ||
accessToken = tokenProvider.generateToken(user, Duration.ofHours(2), "access"); | ||
refreshToken = tokenProvider.generateToken(user, Duration.ofDays(14), "refresh"); | ||
log.info("access: " + accessToken); | ||
log.info("refresh : " + refreshToken); | ||
|
||
//Refresh 토큰 저장 | ||
redisUtils.setData(user.getId().toString(), refreshToken, Duration.ofDays(14).toMillis()); | ||
} |
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.
신규 생성된 회원일경우 (DB에 회원정보가 없을 때) 에 대한 처리가 없습니다.
// HTTP Header 생성 | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.add("Authorization", "Bearer " + accessToken); | ||
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); |
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.
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
으로 대체할 수 있습니다.
|
||
// HTTP Header 생성 | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); |
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.
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
으로 대체할 수 있습니다.
private JsonNode getKakaoUserInfo(String accessToken) { | ||
// HTTP Header 생성 | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.add("Authorization", "Bearer " + accessToken); |
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.
headers.setBearerAuth(accessToken);
으로 대체할 수 있습니다.
이 부분 동작하는지 확인해주세요 컨트롤러
서비스
|
넵 피드백 주신 부분들 수정해서 커밋했습니다. Date 부분은 수정해도 jwt 토큰 생성할 때 계속 Date로 변경하라고 해서 일단은 그대로 둔 상태입니다. 그외 반영 못한 피드백들은 학습이 필요하거나 이해하는데 시간이 좀 걸리는 부분들이라 제 선에서는 넘어가는 점 양해 부탁드릴게요..
|
아 저렇게 되는군요! |
af46929
to
aadaa25
Compare
bf6020c
to
3e6551d
Compare
3e6551d
to
3045b28
Compare
이거 문제가 유은님 문제가 아닌 것 같아서 |
넵 |
요약
카카오 로그인 구현
작업 내용
기타 (논의하고 싶은 부분)
프로필 생성 전 로그인 : 토큰 발급 X
프로필 생성 후 로그인 : 토큰 발급 O
유효하지 않은 인가코드 삽입 삽입시
타 직군 전달 사항
@froggy1014
로그인 기능은 구현됐으나 프로필 생성 api로 회원 정보가 DB에 등록되어야 JWT 토큰이 발급될 것 같습니다.
-> 08/17(토) 중으로 구현 예정
로컬 DB에서 임시로 유저 데이터 생성 후 테스트한 결과 JWT 토큰 발급 및 레디스 저장까지 확인했습니다.
close #53