Skip to content

Commit

Permalink
feat: 카카오 로그인 구현 (#161)
Browse files Browse the repository at this point in the history
* feat: 카카오 프로필 응답 받기

* feat: 카카오 로그인 후 세션 발급
  • Loading branch information
gitchannn authored Mar 20, 2024
1 parent 848656f commit fd80c38
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package sunflower.server.auth.api;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -25,8 +26,14 @@ public ResponseEntity<Void> login() {
}

@GetMapping("/login/kakao/session")
public ResponseEntity<Void> authorize(@RequestParam("code") String code) {
kakaoMemberService.login(code);
return null;
public ResponseEntity<Void> authorize(@RequestParam("code") final String code, final HttpServletResponse response) {
final Long memberId = kakaoMemberService.login(code);
final String sessionId = sessionService.createSessionId(memberId);

response.setHeader("Set-Cookie", "sessionId=" + sessionId + "; HttpOnly; Max-Age=3600; Path=/; Secure; SameSite=None");

return ResponseEntity
.status(HttpStatus.OK.value())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import org.springframework.stereotype.Service;
import sunflower.server.auth.client.KakaoAccessTokenClient;
import sunflower.server.auth.client.KakaoUserProfileClient;
import sunflower.server.auth.client.response.KakaoOAuthResponse;
import sunflower.server.auth.client.response.KakaoAccessTokenResponse;
import sunflower.server.auth.client.response.KakaoUserProfileResponse;
import sunflower.server.entity.LoginType;
import sunflower.server.entity.Member;
import sunflower.server.repository.MemberRepository;

import java.util.Optional;

@Service
public class KakaoMemberService {
Expand All @@ -16,6 +22,7 @@ public class KakaoMemberService {
private final String userProfileURI;
private final KakaoAccessTokenClient kakaoAccessTokenClient;
private final KakaoUserProfileClient kakaoUserProfileClient;
private final MemberRepository memberRepository;

public KakaoMemberService(
@Value("${oauth.kakao.rest-api-key}") String restApiKey,
Expand All @@ -24,7 +31,8 @@ public KakaoMemberService(
@Value("${oauth.kakao.token-uri}") String authTokenURI,
@Value("${oauth.kakao.user-info-request-uri}") String userProfileURI,
final KakaoAccessTokenClient kakaoAccessTokenClient,
final KakaoUserProfileClient kakaoUserProfileClient
final KakaoUserProfileClient kakaoUserProfileClient,
final MemberRepository memberRepository
) {
this.redirectURI = redirectURI;
this.restApiKey = restApiKey;
Expand All @@ -33,17 +41,36 @@ public KakaoMemberService(
this.userProfileURI = userProfileURI;
this.kakaoAccessTokenClient = kakaoAccessTokenClient;
this.kakaoUserProfileClient = kakaoUserProfileClient;
this.memberRepository = memberRepository;
}

public String loginURI() {
return String.format(authCodeURI, restApiKey, redirectURI);
}

public String login(final String code) {
final KakaoOAuthResponse response = kakaoAccessTokenClient.requestAccessToken(authTokenURI, restApiKey, redirectURI, code);
final String accessToken = response.getAccessToken();
kakaoUserProfileClient.requestUserProfile(userProfileURI, accessToken);
public Long login(final String code) {
final KakaoAccessTokenResponse kakaoAccessTokenResponse = kakaoAccessTokenClient.requestAccessToken(authTokenURI, restApiKey, redirectURI, code);
final String accessToken = kakaoAccessTokenResponse.getAccessToken();
final KakaoUserProfileResponse kakaoUserProfileResponse = kakaoUserProfileClient.requestUserProfile(userProfileURI, accessToken);
final Long oauthId = kakaoUserProfileResponse.getOauthId();
final Member member = findOrCreateMember(kakaoUserProfileResponse, oauthId);
return member.getId();
}

private Member findOrCreateMember(final KakaoUserProfileResponse kakaoUserProfileResponse, final Long oauthId) {
final Optional<Member> findMember = memberRepository.findByLoginTypeAndOauthId(LoginType.KAKAO, oauthId);

if (findMember.isPresent()) {
return findMember.get();
}

return null;
return memberRepository.save(
Member
.oauth()
.loginType(LoginType.KAKAO)
.nickname(kakaoUserProfileResponse.getNickname())
.oauthId(kakaoUserProfileResponse.getOauthId())
.build()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ public class MemberService {
private final MemberRepository memberRepository;

public Long join(final String loginId, final String password) {
final Member member = Member.of(loginId, password);
final Member member = Member
.basicLogin()
.loginId(loginId)
.password(password)
.build();
return memberRepository.save(member).getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import sunflower.server.auth.client.response.KakaoOAuthResponse;
import sunflower.server.auth.client.response.KakaoAccessTokenResponse;

@Slf4j
@Component
Expand All @@ -22,7 +22,7 @@ public KakaoAccessTokenClient(final RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public KakaoOAuthResponse requestAccessToken(final String authTokenURI, final String restApiKey, final String redirectURI, final String code) {
public KakaoAccessTokenResponse requestAccessToken(final String authTokenURI, final String restApiKey, final String redirectURI, final String code) {
final String requestURI = authTokenURI;
HttpHeaders requestHeader = createRequestHeader();
final MultiValueMap<String, Object> requestBody = createRequestBody(restApiKey, redirectURI, code);
Expand All @@ -32,7 +32,7 @@ public KakaoOAuthResponse requestAccessToken(final String authTokenURI, final St
log.info("Request Headers: {}", requestHeader);
log.info("Request Parameters: {}", requestBody);

return restTemplate.postForObject(requestURI, requestEntity, KakaoOAuthResponse.class);
return restTemplate.postForObject(requestURI, requestEntity, KakaoAccessTokenResponse.class);
}

private HttpHeaders createRequestHeader() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
package sunflower.server.auth.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import sunflower.server.auth.client.response.KakaoUserProfileResponse;

@Slf4j
@Component
public class KakaoUserProfileClient {

private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;

public KakaoUserProfileClient(
final RestTemplate restTemplate
final RestTemplate restTemplate,
final ObjectMapper objectMapper
) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}

public void requestUserProfile(
public KakaoUserProfileResponse requestUserProfile(
final String requestURI,
final String accessToken
) {
HttpHeaders requestHeader = createRequestHeader(accessToken);
final HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(null, requestHeader);
final MultiValueMap<String, Object> requestBody = createRequestBody();
final HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(requestBody, requestHeader);

log.info("Request URI: {}", requestURI);
log.info("Request Headers: {}", requestHeader);
Expand All @@ -35,6 +43,16 @@ public void requestUserProfile(

final String responseBody = response.getBody();
System.out.println("responseBody = " + responseBody);

try {
final JsonNode responseJson = objectMapper.readTree(responseBody);
final long id = responseJson.path("id").asLong();
final String nickname = responseJson.path("properties").path("nickname").asText();
return new KakaoUserProfileResponse(nickname, id);
} catch (Exception e) {
log.error("Error parsing JSON response: {}", e.getMessage());
throw new IllegalArgumentException(e);
}
}

private HttpHeaders createRequestHeader(final String accessToken) {
Expand All @@ -43,4 +61,10 @@ private HttpHeaders createRequestHeader(final String accessToken) {
requestHeader.set(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
return requestHeader;
}

private MultiValueMap<String, Object> createRequestBody() {
MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>();
requestBody.add("property_keys", "[\"kakao_account.email\"]");
return requestBody;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@ToString
@Getter
@NoArgsConstructor
public class KakaoOAuthResponse {
public class KakaoAccessTokenResponse {

@JsonProperty("access_token")
private String accessToken;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sunflower.server.auth.client.response;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class KakaoUserProfileResponse {

private final String nickname;
private final Long oauthId;
}
9 changes: 9 additions & 0 deletions server/src/main/java/sunflower/server/entity/LoginType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sunflower.server.entity;

public enum LoginType {

BASIC,
KAKAO,
GOOGLE,
;
}
56 changes: 40 additions & 16 deletions server/src/main/java/sunflower/server/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sunflower.server.util.PasswordUtil;

import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.GenerationType.IDENTITY;

@Getter
Expand All @@ -18,23 +21,55 @@ public class Member {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String name;
private String nickname;

@Column(unique = true)
private String loginId;
private String encryptedPassword;

@Enumerated(STRING)
private LoginType loginType;
private Long oauthId;
private Boolean isBlind;

public Member(final Long id, final String name, final String loginId, final String encryptedPassword, final Boolean isBlind) {
public Member(
final Long id,
final String nickname,
final String loginId,
final String encryptedPassword,
final LoginType loginType,
final Long oauthId,
final Boolean isBlind
) {
this.id = id;
this.name = name;
this.nickname = nickname;
this.loginId = loginId;
this.encryptedPassword = encryptedPassword;
this.loginType = loginType;
this.oauthId = oauthId;
this.isBlind = isBlind;
}

public static Member of(final String loginId, final String password) {
return new Member(null, null, loginId, PasswordUtil.encrypt(password), null);
@Builder(builderMethodName = "basicLogin")
public Member(
final String nickname,
final String loginId,
final String password
) {
this.nickname = nickname;
this.loginId = loginId;
this.encryptedPassword = password;
}

@Builder(builderMethodName = "oauth")
public Member(
final String nickname,
final LoginType loginType,
final Long oauthId
) {
this.nickname = nickname;
this.loginType = loginType;
this.oauthId = oauthId;
}

public void checkPassword(final String password) {
Expand All @@ -43,15 +78,4 @@ public void checkPassword(final String password) {
throw new IllegalAccessError("아이디 또는 비밀번호가 잘못되었습니다.");
}
}

@Override
public String toString() {
return "Member{" +
"id=" + id +
", name='" + name + '\'' +
", loginId='" + loginId + '\'' +
", encryptedPassword='" + encryptedPassword + '\'' +
", isBlind=" + isBlind +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
package sunflower.server.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import sunflower.server.entity.LoginType;
import sunflower.server.entity.Member;

import java.util.NoSuchElementException;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

default Member getByLoginId(String loginId) {
default Member getByLoginId(final String loginId) {
final Optional<Member> member = findByLoginId(loginId);
if (member.isEmpty()) {
throw new NoSuchElementException("Member with login id " + loginId + " not found");
}
return member.get();
}

Optional<Member> findByLoginId(String loginId);
Optional<Member> findByLoginId(final String loginId);

default Member getByLoginTypeAndOauthId(final LoginType loginType, final Long oauthId) {
final Optional<Member> member = findByLoginTypeAndOauthId(loginType, oauthId);
if (member.isEmpty()) {
throw new NoSuchElementException("Member with login type, oauthId " + loginType.name() + oauthId + " not found");
}
return member.get();
}

Optional<Member> findByLoginTypeAndOauthId(final LoginType loginType, final Long oauthId);
}

0 comments on commit fd80c38

Please sign in to comment.