diff --git a/src/main/java/com/oronaminc/join/member/dto/KakaoLoginRequest.java b/src/main/java/com/oronaminc/join/member/dto/KakaoLoginRequest.java new file mode 100644 index 0000000..e6a99e8 --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/dto/KakaoLoginRequest.java @@ -0,0 +1,7 @@ +package com.oronaminc.join.member.dto; + +public record KakaoLoginRequest( + String code, + String state +) { +} diff --git a/src/main/java/com/oronaminc/join/member/dto/KakaoLoginResponse.java b/src/main/java/com/oronaminc/join/member/dto/KakaoLoginResponse.java new file mode 100644 index 0000000..9e0ef7d --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/dto/KakaoLoginResponse.java @@ -0,0 +1,9 @@ +package com.oronaminc.join.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record KakaoLoginResponse( + @Schema(description = "회원 id", example = "1001") + Long id +) { +} diff --git a/src/main/java/com/oronaminc/join/member/dto/KakaoUserResponse.java b/src/main/java/com/oronaminc/join/member/dto/KakaoUserResponse.java new file mode 100644 index 0000000..0ce9106 --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/dto/KakaoUserResponse.java @@ -0,0 +1,11 @@ +package com.oronaminc.join.member.dto; + +import lombok.Builder; + +@Builder +public record KakaoUserResponse( + String email, + String nickname, + String profileImageUrl +) { +} diff --git a/src/main/java/com/oronaminc/join/member/security/AuthController.java b/src/main/java/com/oronaminc/join/member/security/AuthController.java index 85b9741..08e5d1f 100644 --- a/src/main/java/com/oronaminc/join/member/security/AuthController.java +++ b/src/main/java/com/oronaminc/join/member/security/AuthController.java @@ -1,10 +1,5 @@ package com.oronaminc.join.member.security; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.tags.Tags; import java.util.List; import org.springframework.http.HttpStatus; @@ -24,8 +19,13 @@ import com.oronaminc.join.member.dto.GuestLoginRequest; import com.oronaminc.join.member.dto.GuestLoginResponse; +import com.oronaminc.join.member.dto.KakaoLoginRequest; +import com.oronaminc.join.member.dto.KakaoLoginResponse; import com.oronaminc.join.member.dto.SessionInfoResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -40,6 +40,33 @@ public class AuthController { private final AuthService authService; + @Operation( + summary = "카카오 로그인", + description = "redirect url 에 포함된 파라미터의 code와 state를 입력해주세요. 이후 모든 요청에 세션 인증이 적용됩니다." + ) + @PostMapping("/kakao") + @ResponseStatus(HttpStatus.OK) + public KakaoLoginResponse kakaoLogin( + @RequestBody KakaoLoginRequest kakaoLoginRequest, + HttpServletRequest request + ) { + MemberDetails memberDetails = authService.kakaoLogin(kakaoLoginRequest.code()); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + memberDetails, null, List.of(new SimpleGrantedAuthority(memberDetails.getRole())) + ); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); + + return new KakaoLoginResponse(memberDetails.getId()); + } + @Operation( summary = "비회원 로그인", description = "닉네임을 입력하면 비회원 세션이 생성되고 인증이 설정됩니다. 이후 모든 요청에 세션 인증이 적용됩니다.", diff --git a/src/main/java/com/oronaminc/join/member/security/AuthService.java b/src/main/java/com/oronaminc/join/member/security/AuthService.java index ba1c2ad..3d64d9f 100644 --- a/src/main/java/com/oronaminc/join/member/security/AuthService.java +++ b/src/main/java/com/oronaminc/join/member/security/AuthService.java @@ -3,19 +3,26 @@ import static com.oronaminc.join.member.util.MemberMapper.*; import java.util.Map; -import java.util.Optional; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import com.oronaminc.join.member.dao.MemberRepository; import com.oronaminc.join.member.domain.Member; import com.oronaminc.join.member.dto.GuestLoginRequest; +import com.oronaminc.join.member.dto.KakaoUserResponse; import com.oronaminc.join.member.service.MemberReader; +import com.oronaminc.join.member.util.MemberMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,32 +34,91 @@ public class AuthService extends DefaultOAuth2UserService { private final MemberRepository memberRepository; private final MemberReader memberReader; - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2User oAuth2User = super.loadUser(userRequest); - Map attributes = oAuth2User.getAttributes(); + private final RestTemplate restTemplate = new RestTemplate(); + + private static final String TOKEN_URI = "https://kauth.kakao.com/oauth/token"; + private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me"; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String redirectUri; + + @Value("${spring.security.oauth2.client.registration.kakao.client-secret}") + private String clientSecret; + + // @Override + // public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + // OAuth2User oAuth2User = super.loadUser(userRequest); + // Map attributes = oAuth2User.getAttributes(); + // + // log.info("attributes :: " + attributes); + // + // Map kakaoAccount = (Map) attributes.get("kakao_account"); + // Map profile = (Map) kakaoAccount.get("profile"); + // + // + // Optional optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString()); + // + // Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile))); + // + // return toOAuth2MemberDetails(member); + // } - log.info("attributes :: " + attributes); + @Transactional + public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) { + Member guest = toGuestMember(guestLoginRequest); - Map kakaoAccount = (Map) attributes.get("kakao_account"); - Map profile = (Map) kakaoAccount.get("profile"); + memberRepository.save(guest); + guest.registerGuest(); + return toGuestMemberDetails(guest); + } - Optional optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString()); + @Transactional + public MemberDetails kakaoLogin(String code) { + String accessToken = getAccessToken(code); + KakaoUserResponse kakaoUser = getUserInfo(accessToken); - Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile))); + Member member = memberRepository.findByEmail(kakaoUser.email()) + .orElseGet(() -> memberRepository.save(MemberMapper.toNewKakaoMember(kakaoUser))); return toOAuth2MemberDetails(member); } - @Transactional - public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) { - Member guest = toGuestMember(guestLoginRequest); + private String getAccessToken(String code) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - memberRepository.save(guest); - guest.registerGuest(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", clientId); + params.add("redirect_uri", redirectUri); + params.add("code", code); + params.add("client_secret", clientSecret); - return toGuestMemberDetails(guest); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.postForEntity(TOKEN_URI, request, Map.class); + + return (String) response.getBody().get("access_token"); } + private KakaoUserResponse getUserInfo(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, Map.class); + + Map attributes = response.getBody(); + + Map kakaoAccount = (Map) attributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + + return MemberMapper.toKakaoUserResponse(kakaoAccount, profile); + } } diff --git a/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java b/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java index fd6861b..582dac7 100644 --- a/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java +++ b/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java @@ -23,9 +23,11 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers( "/api/auth/guest", + "/api/auth/kakao", "/login" ) .anonymous() diff --git a/src/main/java/com/oronaminc/join/member/util/MemberMapper.java b/src/main/java/com/oronaminc/join/member/util/MemberMapper.java index 673fa6d..7835d9b 100644 --- a/src/main/java/com/oronaminc/join/member/util/MemberMapper.java +++ b/src/main/java/com/oronaminc/join/member/util/MemberMapper.java @@ -5,6 +5,7 @@ import com.oronaminc.join.member.domain.Member; import com.oronaminc.join.member.domain.MemberType; import com.oronaminc.join.member.dto.GuestLoginRequest; +import com.oronaminc.join.member.dto.KakaoUserResponse; import com.oronaminc.join.member.security.MemberDetails; import lombok.AccessLevel; @@ -47,4 +48,21 @@ public static Member toKakaoMember(Map kakaoAccount, Map kakaoAccount, Map profile) { + return KakaoUserResponse.builder() + .email((String) kakaoAccount.get("email")) + .nickname((String) profile.get("nickname")) + .profileImageUrl((String) profile.get("profile_image_url")) + .build(); + } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 469322b..c1c6fd1 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -10,6 +10,28 @@ spring: hibernate: ddl-auto: create + security: + oauth2: + client: + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + registration: + kakao: + client-name: Kakao + client-id: KAKAO_CLIENT_ID + client-secret: KAKAO_CLIENT_SECRET + redirect-uri: KAKAO_REDIRECT_URI + authorization-grant-type: authorization_code + client-authentication-method: client_secret_post + scope: + - profile_nickname + - profile_image + - account_email + cloud: aws: region: