이 글은 이틀간의 논스탑바보짓밤샘에러해결쇼의 결과물에 대한 기록입니다.
아하하... 힘들고 막막했지만 그래도 해결했으니 되었다~!
제가 진행하고 있는 프로젝트를 기준으로 작성했습니다.
더 나은 로직이 (⚡️얼마든지⚡️) 있을 수 있으므로 참고하는 용으로만 봐주시고,
보완해야 할 점이나 잘못된 부분이 있으면 댓글로 알려주세요!
기재되어 있지 않은 부분에 대해 궁금하신 분은 댓글로 질문해주시면 아는 선에서 답변 드리겠습니다 👍
AuthService
SecurityService
AuthController
Service 클래스에서는 비즈니스 로직을 처리하기 위한 메소드들을 구현한다.
가독성 고려
컨트롤러 단에서 서비스 메소드들을 일일이 호출할 것인지,
서비스 상에 전체를 아우르는 메소드를 하나 만들어서 그 메소드만 호출할 것인지.
-> 후자 선택. 컨트롤러 단에서는 전체적인 흐름을 확인할 수 있게 하고 싶어서 복잡한 로직은 서비스 단에서 해결하기로 했다.
메소드 재사용성 고려
다른 소셜 로그인을 구현할 때에도 재사용하기 좋은 코드를 짜고 싶었다.
메소드 파라미터로 어떤 것을 받으면 좋을지 고민해서 코드를 짰다.
요청/응답에 대한 body 형식
웹과 JSON 형식으로 응답을 주고받는 상황이고,
로그인 과정의 요청과 응답 양식은 고정적이므로 일일이 JSON body를 작성하는 것 대신 로그인 요청에 대한 DTO를 따로 만들기로 했다.
이를 바탕으로 구상한 메소드들은 다음과 같다.
메소드 설명은 주석으로 달아두었음!
인가코드로 kakaoAccessToken 따오는 메소드.
public kakaoTokenDto getkakaoAccessToken(String code)
파라미터: 인가코드 code
반환형: kakaoTokenDto
public KakaoTokenDto getKakaoAccessToken(String code) {
RestTemplate rt = new RestTemplate(); //통신용
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpBody 객체 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code"); //카카오 공식문서 기준 authorization_code 로 고정
params.add("client_id", KAKAO_CLIENT_ID); //카카오 앱 REST API 키
params.add("redirect_uri", KAKAO_REDIRECT_URI);
params.add("code", code); //인가 코드 요청시 받은 인가 코드값, 프론트에서 받아오는 그 코드
// 헤더와 바디 합치기 위해 HttpEntity 객체 생성
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headers);
System.out.println(kakaoTokenRequest);
// 카카오로부터 Access token 수신
ResponseEntity<String> accessTokenResponse = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
// JSON Parsing (-> KakaoTokenDto)
ObjectMapper objectMapper = new ObjectMapper();
KakaoTokenDto kakaoTokenDto = null;
try {
kakaoTokenDto = objectMapper.readValue(accessTokenResponse.getBody(), KakaoTokenDto.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return kakaoTokenDto;
}
kakaoAccessToken 으로 카카오 로그인 서버에 정보 요청하는 메소드.
public KakaoAccountDto getKakaoInfo(String kakaoAccessToken)
파라미터: 카카오 Access Token kakaoAccessToken
반환형: kakaoAccountDto
public KakaoAccountDto getKakaoInfo(String kakaoAccessToken) {
RestTemplate rt = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + kakaoAccessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
HttpEntity<MultiValueMap<String, String>> accountInfoRequest = new HttpEntity<>(headers);
// POST 방식으로 API 서버에 요청 보내고, response 받아옴
ResponseEntity<String> accountInfoResponse = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
accountInfoRequest,
String.class
);
System.out.println("카카오 서버에서 정상적으로 데이터를 수신했습니다.");
// JSON Parsing (-> kakaoAccountDto)
ObjectMapper objectMapper = new ObjectMapper();
KakaoAccountDto kakaoAccountDto = null;
try {
kakaoAccountDto = objectMapper.readValue(accountInfoResponse.getBody(), KakaoAccountDto.class);
} catch (JsonProcessingException e) { e.printStackTrace(); }
return kakaoAccountDto;
}
kakaoAccountDto 를 Account 객체로 매핑하는 메소드.
public Account mapKakaoInfo(KakaoAccountDto accountDto)
파라미터: KakaoAccountDto kakaoAccountDto
반환형: Account
public Account mapKakaoInfo(KakaoAccountDto accountDto) {
// kakaoAccountDto 에서 필요한 정보 꺼내기
Long kakaoId = accountDto.getId();
String email = accountDto.getKakao_account().getEmail();
String nickname = accountDto.getKakao_account().getProfile().getNickname();
System.out.println("매핑한 정보 : " + email + ", " + nickname);
// Account 객체에 매핑
return Account.builder()
.socialId(kakaoId)
.email(email)
.nickname(nickname)
.role("USER")
.build();
}
🔽 4, 5번은 컨트롤러로 직행(?) 할 전체적인 로직을 담은 메소드들이다.
login 요청 보내는 회원가입 유무 판단해 분기 처리할 메소드.
public LoginResponseDto kakaoLogin(String kakaoAccessToken)
파라미터: String kakaoAccessToken
반환형: LoginResponseDto
public LoginResponseDto kakaoLogin(String kakaoAccessToken) {
// kakaoAccessToken 으로 카카오 회원정보 받아옴
KakaoAccountDto kakaoAccountDto = getKakaoInfo(kakaoAccessToken);
String kakaoEmail = kakaoAccountDto.getKakao_account().getEmail();
// kakaoAccountDto 를 Account 로 매핑
Account selectedAccount = mapKakaoInfo(kakaoAccountDto);
System.out.println("수신된 account 정보 : " + selectedAccount);
// 매핑만 하고 DB에 저장하질 않았으니까 Autogenerated 인 id가 null 로 나왔던거네 아... 오케오케 굿
LoginResponseDto loginResponseDto = new LoginResponseDto();
loginResponseDto.setKakaoAccessToken(kakaoAccessToken);
// 가입되어 있으면 해당하는 Account 객체를 응답
// 존재하면 true + access token 을, 존재하지 않으면 False 리턴
if (accountRepository.existsByEmail(kakaoEmail)) {
Account resultAccount = accountRepository.findByEmail(kakaoEmail);
loginResponseDto.setLoginSuccess(true);
loginResponseDto.setKakaoAccessToken(kakaoAccessToken);
loginResponseDto.setAccount(resultAccount);
System.out.println("response setting: " + loginResponseDto);
// 토큰 발급
TokenDto tokenDto = securityService.login(resultAccount.getId());
loginResponseDto.setAccessToken(tokenDto.getAccessToken());
System.out.println("로그인이 확인됐고, 토큰을 발급했습니다.");
}
return loginResponseDto;
}
회원가입 처리 메소드
public Long kakaoSignUp(SignupRequestDto requestDto)
파라미터: SignupRequestDto requestDto
반환형: Long accountId
public Long kakaoSignUp(SignupRequestDto requestDto) {
KakaoAccountDto kakaoAccountDto = getKakaoInfo(requestDto.getKakaoAccessToken());
Account account = mapKakaoInfo(kakaoAccountDto);
// 닉네임, 프로필사진 set
String accountName = requestDto.getAccountName();
String accountPicture = requestDto.getPicture();
account.setAccountName(accountName);
account.setPicture(accountPicture);
// DB에 save
accountRepository.save(account);
// 회원가입 결과로 회원가입한 accountId 리턴
return account.getId();
}
먼저 모든 로그인 요청을 처리할 API를 만들어봤다.
모든 소셜 로그인 요청에 대해서 발생하는 인가코드를 params로 받아 가입된 사용자인지 아닌지 확인해 결과를 응답해주어야 한다.
request로 온 사용자 정보를 DB에서 검색해 DB에 존재하면 boolean 타입으로 로그인 성공 유무를 true
로 설정하고 사용자 정보를 불러올 수 있는 Access token
을 리턴한다.
가입되지 않은 사용자로 확인되면 로그인 성공 유무를 false
로 설정한다.
@GetMapping("/login/kakao")
public ResponseEntity<LoginResponseDto> kakaoLogin(HttpServletRequest request) {
String code = request.getParameter("code");
System.out.println(code);
KakaoTokenDto kakaoTokenDto = authService.getKakaoAccessToken(code);
System.out.println("kakaoTokenDto: " + kakaoTokenDto);
String kakaoAccessToken = kakaoTokenDto.getAccess_token();
System.out.println("kakaoAccessToken: " + kakaoAccessToken);
// 토큰 발급까지 authService.kakaologin 상에서 다 처리하자
LoginResponseDto loginResponseDto = authService.kakaoLogin(kakaoAccessToken);
return ResponseEntity.ok(loginResponseDto);
}
가입되지 않은 사용자일 경우 프론트 상에서 가입 창으로 리다이렉트 시킬 것이다.
리다이렉트된 컴포넌트에서 state로 닉네임과 프로필 사진을 입력받아 Account 객체에 담고, 이 객체를 repository에 save하도록 했다.
@PostMapping("/signup")
public ResponseEntity<SignupResponseDto> kakaoSignup(@RequestBody SignupRequestDto requestDto) {
// requestDto 로 데이터 받아와서 accountId 반환
Long accountId = authService.kakaoSignUp(requestDto);
// 최초 가입자에게는 RefreshToken, AccessToken 모두 발급
TokenDto tokenDto = securityService.signup(accountId);
// AccessToken 은 header 에 세팅하고, refreshToken 은 httpOnly 쿠키로 세팅
SignupResponseDto signUpResponseDto = new SignupResponseDto();
HttpHeaders headers = new HttpHeaders();
ResponseCookie cookie = ResponseCookie.from("RefreshToken", tokenDto.getRefreshToken())
.maxAge(60*60*24*7) // 쿠키 유효기간 7일로 설정했음
.path("/")
.secure(true)
.sameSite("None")
.httpOnly(true)
.build();
headers.add("Set-Cookie", cookie.toString());
headers.add("Authorization", tokenDto.getAccessToken());
signUpResponseDto.setResult("가입이 완료되었습니다.");
return ResponseEntity.ok().headers(headers).body(signUpResponseDto);
}
loginSuccess = false
리턴발급받은 정보로 회원가입 시도
다시 로그인 시도
전체 코드 확인하기 ➡️ 🐱 깃허브 🐱
다음 글에서는 토큰 발급 및 검증 과정을 정리해볼 예정이다.
다음 글은 안올라오나유..