Skip to content

Commit bdcf47a

Browse files
authored
feat: 카카오 로그인 API (#86)
* fix: cors disable 추가 * feat: 카카오 로그인 API * feat: 시큐리티에 카카오 로그인 url 추가 * docs: swagger 수정 * test: test yml 수정
1 parent 4d341f8 commit bdcf47a

File tree

8 files changed

+186
-24
lines changed

8 files changed

+186
-24
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
public record KakaoLoginRequest(
4+
String code,
5+
String state
6+
) {
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record KakaoLoginResponse(
6+
@Schema(description = "회원 id", example = "1001")
7+
Long id
8+
) {
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
import lombok.Builder;
4+
5+
@Builder
6+
public record KakaoUserResponse(
7+
String email,
8+
String nickname,
9+
String profileImageUrl
10+
) {
11+
}

src/main/java/com/oronaminc/join/member/security/AuthController.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
package com.oronaminc.join.member.security;
22

3-
import io.swagger.v3.oas.annotations.Operation;
4-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
5-
import io.swagger.v3.oas.annotations.responses.ApiResponses;
6-
import io.swagger.v3.oas.annotations.tags.Tag;
7-
import io.swagger.v3.oas.annotations.tags.Tags;
83
import java.util.List;
94

105
import org.springframework.http.HttpStatus;
@@ -24,8 +19,13 @@
2419

2520
import com.oronaminc.join.member.dto.GuestLoginRequest;
2621
import com.oronaminc.join.member.dto.GuestLoginResponse;
22+
import com.oronaminc.join.member.dto.KakaoLoginRequest;
23+
import com.oronaminc.join.member.dto.KakaoLoginResponse;
2724
import com.oronaminc.join.member.dto.SessionInfoResponse;
2825

26+
import io.swagger.v3.oas.annotations.Operation;
27+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
28+
import io.swagger.v3.oas.annotations.tags.Tag;
2929
import jakarta.servlet.http.Cookie;
3030
import jakarta.servlet.http.HttpServletRequest;
3131
import jakarta.servlet.http.HttpServletResponse;
@@ -40,6 +40,33 @@
4040
public class AuthController {
4141
private final AuthService authService;
4242

43+
@Operation(
44+
summary = "카카오 로그인",
45+
description = "redirect url 에 포함된 파라미터의 code와 state를 입력해주세요. 이후 모든 요청에 세션 인증이 적용됩니다."
46+
)
47+
@PostMapping("/kakao")
48+
@ResponseStatus(HttpStatus.OK)
49+
public KakaoLoginResponse kakaoLogin(
50+
@RequestBody KakaoLoginRequest kakaoLoginRequest,
51+
HttpServletRequest request
52+
) {
53+
MemberDetails memberDetails = authService.kakaoLogin(kakaoLoginRequest.code());
54+
55+
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
56+
memberDetails, null, List.of(new SimpleGrantedAuthority(memberDetails.getRole()))
57+
);
58+
59+
SecurityContext context = SecurityContextHolder.createEmptyContext();
60+
context.setAuthentication(authentication);
61+
SecurityContextHolder.setContext(context);
62+
63+
SecurityContextHolder.getContext().setAuthentication(authentication);
64+
65+
request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
66+
67+
return new KakaoLoginResponse(memberDetails.getId());
68+
}
69+
4370
@Operation(
4471
summary = "비회원 로그인",
4572
description = "닉네임을 입력하면 비회원 세션이 생성되고 인증이 설정됩니다. 이후 모든 요청에 세션 인증이 적용됩니다.",

src/main/java/com/oronaminc/join/member/security/AuthService.java

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
33
import static com.oronaminc.join.member.util.MemberMapper.*;
44

55
import java.util.Map;
6-
import java.util.Optional;
76

7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.http.HttpEntity;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.HttpMethod;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.http.ResponseEntity;
813
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
9-
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
10-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
11-
import org.springframework.security.oauth2.core.user.OAuth2User;
1214
import org.springframework.stereotype.Service;
1315
import org.springframework.transaction.annotation.Transactional;
16+
import org.springframework.util.LinkedMultiValueMap;
17+
import org.springframework.util.MultiValueMap;
18+
import org.springframework.web.client.RestTemplate;
1419

1520
import com.oronaminc.join.member.dao.MemberRepository;
1621
import com.oronaminc.join.member.domain.Member;
1722
import com.oronaminc.join.member.dto.GuestLoginRequest;
23+
import com.oronaminc.join.member.dto.KakaoUserResponse;
1824
import com.oronaminc.join.member.service.MemberReader;
25+
import com.oronaminc.join.member.util.MemberMapper;
1926

2027
import lombok.RequiredArgsConstructor;
2128
import lombok.extern.slf4j.Slf4j;
@@ -27,32 +34,91 @@ public class AuthService extends DefaultOAuth2UserService {
2734
private final MemberRepository memberRepository;
2835
private final MemberReader memberReader;
2936

30-
@Override
31-
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
32-
OAuth2User oAuth2User = super.loadUser(userRequest);
33-
Map<String, Object> attributes = oAuth2User.getAttributes();
37+
private final RestTemplate restTemplate = new RestTemplate();
38+
39+
private static final String TOKEN_URI = "https://kauth.kakao.com/oauth/token";
40+
private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
41+
42+
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
43+
private String clientId;
44+
45+
@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
46+
private String redirectUri;
47+
48+
@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
49+
private String clientSecret;
50+
51+
// @Override
52+
// public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
53+
// OAuth2User oAuth2User = super.loadUser(userRequest);
54+
// Map<String, Object> attributes = oAuth2User.getAttributes();
55+
//
56+
// log.info("attributes :: " + attributes);
57+
//
58+
// Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
59+
// Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
60+
//
61+
//
62+
// Optional<Member> optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString());
63+
//
64+
// Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile)));
65+
//
66+
// return toOAuth2MemberDetails(member);
67+
// }
3468

35-
log.info("attributes :: " + attributes);
69+
@Transactional
70+
public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) {
71+
Member guest = toGuestMember(guestLoginRequest);
3672

37-
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
38-
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
73+
memberRepository.save(guest);
74+
guest.registerGuest();
3975

76+
return toGuestMemberDetails(guest);
77+
}
4078

41-
Optional<Member> optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString());
79+
@Transactional
80+
public MemberDetails kakaoLogin(String code) {
81+
String accessToken = getAccessToken(code);
82+
KakaoUserResponse kakaoUser = getUserInfo(accessToken);
4283

43-
Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile)));
84+
Member member = memberRepository.findByEmail(kakaoUser.email())
85+
.orElseGet(() -> memberRepository.save(MemberMapper.toNewKakaoMember(kakaoUser)));
4486

4587
return toOAuth2MemberDetails(member);
4688
}
4789

48-
@Transactional
49-
public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) {
50-
Member guest = toGuestMember(guestLoginRequest);
90+
private String getAccessToken(String code) {
91+
HttpHeaders headers = new HttpHeaders();
92+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
5193

52-
memberRepository.save(guest);
53-
guest.registerGuest();
94+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
95+
params.add("grant_type", "authorization_code");
96+
params.add("client_id", clientId);
97+
params.add("redirect_uri", redirectUri);
98+
params.add("code", code);
99+
params.add("client_secret", clientSecret);
54100

55-
return toGuestMemberDetails(guest);
101+
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
102+
103+
ResponseEntity<Map> response = restTemplate.postForEntity(TOKEN_URI, request, Map.class);
104+
105+
return (String) response.getBody().get("access_token");
56106
}
57107

108+
private KakaoUserResponse getUserInfo(String accessToken) {
109+
HttpHeaders headers = new HttpHeaders();
110+
headers.setBearerAuth(accessToken);
111+
headers.setContentType(MediaType.APPLICATION_JSON);
112+
113+
HttpEntity<Void> entity = new HttpEntity<>(headers);
114+
115+
ResponseEntity<Map> response = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, Map.class);
116+
117+
Map<String, Object> attributes = response.getBody();
118+
119+
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
120+
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
121+
122+
return MemberMapper.toKakaoUserResponse(kakaoAccount, profile);
123+
}
58124
}

src/main/java/com/oronaminc/join/member/security/SecurityConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ public class SecurityConfig {
2323
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2424
return http
2525
.csrf(csrf -> csrf.disable())
26+
.cors(cors -> cors.disable())
2627
.authorizeHttpRequests(auth -> auth
2728
.requestMatchers(
2829
"/api/auth/guest",
30+
"/api/auth/kakao",
2931
"/login"
3032
)
3133
.anonymous()

src/main/java/com/oronaminc/join/member/util/MemberMapper.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.oronaminc.join.member.domain.Member;
66
import com.oronaminc.join.member.domain.MemberType;
77
import com.oronaminc.join.member.dto.GuestLoginRequest;
8+
import com.oronaminc.join.member.dto.KakaoUserResponse;
89
import com.oronaminc.join.member.security.MemberDetails;
910

1011
import lombok.AccessLevel;
@@ -47,4 +48,21 @@ public static Member toKakaoMember(Map<String, Object> kakaoAccount, Map<String,
4748
.memberType(MemberType.MEMBER)
4849
.build();
4950
}
51+
52+
public static Member toNewKakaoMember(KakaoUserResponse kakaoUser) {
53+
return Member.builder()
54+
.email(kakaoUser.email())
55+
.nickname(kakaoUser.nickname())
56+
.profileImage(kakaoUser.profileImageUrl())
57+
.memberType(MemberType.MEMBER)
58+
.build();
59+
}
60+
61+
public static KakaoUserResponse toKakaoUserResponse(Map<String, Object> kakaoAccount, Map<String, Object> profile) {
62+
return KakaoUserResponse.builder()
63+
.email((String) kakaoAccount.get("email"))
64+
.nickname((String) profile.get("nickname"))
65+
.profileImageUrl((String) profile.get("profile_image_url"))
66+
.build();
67+
}
5068
}

src/test/resources/application.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ spring:
1010
hibernate:
1111
ddl-auto: create
1212

13+
security:
14+
oauth2:
15+
client:
16+
provider:
17+
kakao:
18+
authorization-uri: https://kauth.kakao.com/oauth/authorize
19+
token-uri: https://kauth.kakao.com/oauth/token
20+
user-info-uri: https://kapi.kakao.com/v2/user/me
21+
user-name-attribute: id
22+
registration:
23+
kakao:
24+
client-name: Kakao
25+
client-id: KAKAO_CLIENT_ID
26+
client-secret: KAKAO_CLIENT_SECRET
27+
redirect-uri: KAKAO_REDIRECT_URI
28+
authorization-grant-type: authorization_code
29+
client-authentication-method: client_secret_post
30+
scope:
31+
- profile_nickname
32+
- profile_image
33+
- account_email
34+
1335
cloud:
1436
aws:
1537
region:

0 commit comments

Comments
 (0)