이번 포스팅은 싸피 공통 프로젝트에서 SpringSecurity 환경에서 OAuth 방식으로 JWT을 주고 받으며 RESTful로 카카오와 네이버 로그인을 구현했다. 구현한 과정을 정리해보자! 위 사진은 프로젝트에서 로그인 화면이다.
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
위 주소는 카카오 로그인 api와 관한 문서다.
처음에 카카오 인가 코드를 받아야 한다.
우리가 흔히 보는 카카오 로그인 창에서 이메일과 비밀번호를 입력하면 카카오에서 redirect_url로 주소로 보내고 url 끝에 code = xxxxxxxxxx 값을 준다.
code 값을 Spring으로 던져주면 Controller에서 code 값을 받고 시작하면 된다.
@Operation(summary = "카카오로 로그인 및 회원가입", description = "카카오로 로그인 및 회원가입 하는 API")
@PostMapping("/api/members/kakao/login")
public ResponseEntity<LoginResponseDto> loginKakao(@RequestBody LoginRequestDto codeRequest) {
LoginDto member = memberService.findKakaoMemberByAuthorizedCode(codeRequest.getCode(), RedirectUrlProperties.KAKAO_REDIRECT_URL);
String accessToken = jwtTokenProvider.createAccessToken(member.getMemberId(), member.getSocialId(), member.getSocialType());
String refreshToken = jwtTokenProvider.createRefreshToken(member.getMemberId());
jwtTokenProvider.storeRefreshToken(member.getMemberId(), refreshToken);
return ResponseEntity.ok()
.header("Authorization", accessToken)
.header("Authorization-Refresh",refreshToken)
.body(LoginResponseDto.builder()
.message("카카오 로그인을 성공하셨습니다")
.memberId(member.getMemberId())
.nickname(member.getNickname())
.firstLogin(member.isFirstLogin())
.build());
}
@Transactional
public LoginDto findKakaoMemberByAuthorizedCode(String authorizedCode, String redirectUri) {
// 카카오 OAuth2 를 통해 카카오 사용자 정보 조회
KakaoMemberDto kakaoUserDto = kakaoOAuth2.getMemberInfo(authorizedCode, redirectUri);
String email = kakaoUserDto.getEmail();
String socialId = kakaoUserDto.getSocialId();
Optional<Member> optionalMember = memberRepository.findBySocialId(socialId);
if(optionalMember.isPresent()){
return LoginDto.builder()
.memberId(optionalMember.get().getMemberId())
.socialId(optionalMember.get().getSocialId())
.socialType(optionalMember.get().getSocialType())
.nickname(optionalMember.get().getNickname())
.firstLogin(false)
.build();
}
// 가입된 유저가 아니라면 회원가입 진행
else {
String profilePath = kakaoUserDto.getProfilePath();
Member member = Member.builder()
.role(Role.USER)
.email(email)
.nickname(UUID.randomUUID()+"hello")
.profilePath(null)
.socialId(socialId)
.socialType(Social.KAKAO)
.build();
Member saveMember = memberRepository.save(member);
ShowInfo showInfo = ShowInfo.builder()
.member(saveMember)
.build();
showInfoRepository.save(showInfo);
return LoginDto.builder()
.memberId(member.getMemberId())
.socialId(member.getSocialId())
.socialType(member.getSocialType())
.nickname(member.getNickname())
.firstLogin(true)
.build();
}
}
public KakaoMemberDto getMemberInfo(String authorizedCode, String redirectUri) {
// 1. 인가코드 -> 액세스 토큰
String accessToken = getAccessToken(authorizedCode, redirectUri);
// 2. 액세스 토큰 -> 카카오 사용자 정보
return getMemberByAccessToken(accessToken);
}
private String getAccessToken(String authorizedCode, String redirectUri) {
// HttpHeader 오브젝트 생성
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");
params.add("client_id", clientId);
params.add("redirect_uri", redirectUri);
params.add("code", authorizedCode);
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers);
// Http 요청
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
// JSON -> 액세스 토큰 파싱
String tokenJson = response.getBody();
JSONObject json = new JSONObject(tokenJson);
return json.getString("access_token");
}
private KakaoMemberDto getMemberByAccessToken(String accessToken) {
JSONObject body = requestMemberInfoJsonObject(accessToken);
System.out.println("body = " + body);
// 유저 정보 파싱
JSONObject kakaoAccount = body.getJSONObject("kakao_account");
JSONObject profile = kakaoAccount.getJSONObject("profile");
String email = kakaoAccount.getString("email");
String nickname = body.getJSONObject("properties").getString("nickname");
String profilePhoto = profile.getString("profile_image_url");
return KakaoMemberDto.builder()
.socialId(Long.toString((Long) body.get("id")))
.email(email)
.name(nickname)
.profilePath(profilePhoto)
.build();
}
private JSONObject requestMemberInfoJsonObject(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers);
// Http 요청하기
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoProfileRequest,
String.class
);
public String createAccessToken(Integer id, String userPk, Social socialType) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
claims.put("id", id);
claims.put("socialType",socialType);
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
public String createRefreshToken(Integer id) {
Date now = new Date();
return Jwts.builder()
.setId(Integer.toString(id)) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
public void storeRefreshToken(int id, String refreshToken) {
Member member = memberRepository.findById(id).orElse(null);
if (member != null) {
redisTemplate.opsForValue().set(
Integer.toString(id),
refreshToken,
refreshTokenValidTime,
TimeUnit.MILLISECONDS
);
}
}
다시 controller로 돌아와서 카카오 사용자 정보 Dto를 return 받았고, JwtTokenProvider에서 엑세스 토큰과 리프레시 토큰을 생성한다.
생성한 리프레시 토큰은 redis에 저장시킨다.
생성한 엑세스 토큰과 리프레시 토큰은 header에 담아서 client에게 응답해준다.
이렇게 카카오 소셜 로그인 구현을 완료했고 네이버도 똑같이 구현했다.
다음 포스팅은 리프레시 토큰을 이용해서 토큰을 재발급 받는 과정을 정리해보자!!