Java/환경구성
[Java] OAuth 2.0 카카오 개발자(Kakao Developers) 동의항목 권한 없음 해결 방법 : 테스트 앱 생성
adjh54
2024. 11. 9. 23:26
반응형
해당 글에서는 카카오 개발자(Kakao Developers) 동의항목 중 권한 없음에 대한 해결 방법으로 테스트 앱 생성 방법을 이용하여 권한을 받는 방법에 대해 알아봅니다.
1) 문제점 확인
💡 문제점 확인
- 카카오 로그인을 수행하였을 때, 추가적인 정보를 받고자 할 때 이에 대한 “권한 없음”으로 선택이 되지 않는 문제가 있습니다.
- 이러한 권한을 얻기 위해서는 ‘비즈니스 인증’을 완료해야합니다.
1. 비즈니스 인증
💡 비즈니스 인증
- 앱 설정 > 앱 권한 신청 경로 탭으로 이동하면 카카오 로그인을 수행했을 때, 추가적인 정보를 받을 수 있도록 비즈니스에 대한 정보를 위해서는 심사가 필요합니다.
💡비즈니스 정보 > 신청 버튼 클릭
- 아래와 같이 비즈니스 신청을 하면 필요로 하는 ‘개인정보 동의항목’이 포함되어 있습니다.
💡 다음 버튼을 누릅니다
- 아래와 같이 서비스 정보에 대한 URL에 필요하기에, “테스트”로 사용하는 경우에는 별도의 URL이 존재하지 않기에 적을 수 있는 URL이 없습니다.
2) 해결 방법 : 테스트 앱 생성
💡 해결 방법 : 테스트 앱 생성
- 아래의 공식 사이트를 기반으로 테스트 앱을 생성하여서 이를 해결합니다.
1. 내 애플리케이션 > 일반 > 테스트 앱 정보로 이동하여 '테스트앱 생성' 버튼을 누릅니다.
2. 아래와 같은 팝업이 출력되고 이름을 설정하고 ‘저장’ 버튼을 선택해 줍니다.
3. 전체 애플리케이션 페이지로 이동합니다.
💡 전체 애플리케이션 페이지로 이동합니다.
- 아래와 같이 -TEST라는 형태의 애플리케이션이 추가되었습니다. 해당 앱을 선택합니다.
4. 앱 설정 > 대시보드 페이지에서 카카오 로그인 ‘설정하기’ 버튼을 누릅니다.
5. 제품 설정 > 카카오 로그인 > 활성화 설정 상태를 'ON'으로 변경합니다.
6. 제품 설정 > 동의항목 페이지로 이동하면 동의 항목의 권한 없음 문제가 해결되었습니다.
3) 실제 테스트
💡 실제 테스트
- 해당 계정의 정보와 연결하여서 실제 OAuth 2.0 테스트로 값이 전달되는지 확인해 봅니다.
- 아래와 같이 주요 개인정보에 대해 ‘필수 동의’로 적용하였습니다.
💡 [참고] 아래의 설정 방법을 기반으로 설정하였습니다.
2. 권한 요청 페이지 출력
💡 권한 요청 페이지 출력
- 아래와 같이 -TEST 애플리케이션을 바라보게 구성이 되었고, 필수 동의 사항에 위에 설정한 내용을 받아오도록 설정이 되어 있습니다.
3. 권한 불러오기 설정
💡 권한 불러오기 설정
- 아래와 같은 Scope의 PropertyKeys 내에 값을 받습니다.
속성 | 설명 |
kakao_account.profile | 카카오계정의 프로필 소유 여부실시간 닉네임과 프로필 사진 URL |
kakao_account.name | 카카오계정의 이름 소유 여부, 이름 값 |
kakao_account.email | 카카오계정의 이메일 소유 여부이메일 값, 이메일 인증 여부, 이메일 유효 여부 |
kakao_account.age_range | 카카오계정의 연령대 소유 여부, 연령대 값 |
kakao_account.birthday | 카카오계정의 생일 소유 여부, 생일 값 |
kakao_account.gender | 카카오계정의 성별 소유 여부, 성별 값 |
3.1. application.oauth2.yml 파일
💡 application.oauth2.yml 파일
- scope 내에서 불러올 내용을 모두 추가하였습니다.
spring:
security:
oauth2:
client:
# OAuth2 인증 제공자에 대한 설정 정보를 포함합니다.
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
# 클라이언트 애플리케이션(Spring Boot)에 대한 설정을 포함합니다.
registration:
kakao:
client-id: xx
client-secret: xx
redirect-uri:
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: kakao
scope:
- kakao_account.profile
- kakao_account.name
- kakao_account.email
- kakao_account.age_range
- kakao_account.birthday
- kakao_account.gender
3.2. 반환 객체 구성 : OAuth2KakaoUserInfoDto
💡 반환 객체 구성 : OAuth2KakaoUserInfoDto
- 기존의 Biz가 아닌 상태에서 받았던 내용 외에 모든 정보를 불러오도록 하였습니다.
package com.adjh.springbootoauth2.dto.oauth2;
import lombok.*;
/**
* Please explain the class!!
*
* @author : jonghoon
* @fileName : OAuth2KakaoUserInfoDto
* @since : 11/8/24
*/
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuth2KakaoUserInfoDto {
private String id;
private int statusCode; // 상태 코드
private String email; // 이메일
private String nickname; // 닉네임
private String profileImageUrl; // 프로필 이미지 URL
private String thumbnailImageUrl; // 썸네일 이미지 URL
private String name; // [Biz] 사용자 이름
private String ageRange; // [Biz] 사용자 나이 범위
private String birthday; // [Biz] 사용자 생일
private String gender; // [Biz] 사용자 성별
@Builder
public OAuth2KakaoUserInfoDto(String id, int statusCode, String email, String nickname, String profileImageUrl, String thumbnailImageUrl, String name, String ageRange, String birthday, String gender) {
this.id = id;
this.statusCode = statusCode;
this.email = email;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
this.thumbnailImageUrl = thumbnailImageUrl;
this.name = name;
this.ageRange = ageRange;
this.birthday = birthday;
this.gender = gender;
}
}
3.3. 비즈니스 로직에서 로드
💡 비즈니스 로직에서 로드
- 해당 메서드 내에서 사용자 정보를 요청하는 파라미터로 “property_keys” 키에 값으로 oAuthRegistration.kakao().scope()에서 불러온 내용을 기반으로 사용자 정보를 요청하고 있습니다.
@Slf4j
@Service("OAuth2ServiceImpl")
public class OAuth2ServiceImpl implements OAuth2Service {
/**
* 카카오 로그인 사용자 정보를 가져옵니다.
*
* @param accessToken
* @return
* @refrence <https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info>
*/
private OAuth2KakaoUserInfoDto getKakaoUserInfo(String accessToken) {
log.debug("[+] getKakaoUserInfo을 수행합니다 :: {}", accessToken);
ResponseEntity<map<string, object="">> responseUserInfo = null;
OAuth2KakaoUserInfoDto resultDto = null;
// [STEP1] 필수 요청 Header 값 구성 : ContentType, Authorization
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
headers.add("Authorization", "Bearer " + accessToken); // accessToken 추가
// [STEP2] 요청 파라미터 구성 : 원하는 사용자 정보
MultiValueMap<string, object=""> userInfoParam = new LinkedMultiValueMap<>();
ObjectMapper objectMapper = new ObjectMapper();
try {
userInfoParam.add("property_keys", objectMapper.writeValueAsString(oAuthRegistration.kakao().scope())); // 불러올 데이터 조회 (리스트 to 문자열 변환)
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
HttpEntity<multivaluemap<string, object="">> userInfoReq = new HttpEntity<>(userInfoParam, headers);
// [STEP3] 요청 Header, 파라미터를 포함하여 사용자 정보 조회 URL로 요청을 수행합니다.
try {
responseUserInfo = restTemplateConfig
.restTemplate()
.exchange(oAuthProvider.kakao().userInfoUri(), HttpMethod.POST, userInfoReq, new ParameterizedTypeReference<>() {
});
log.debug("결과 값 :: {}", responseUserInfo);
} catch (Exception e) {
log.error("[-] 사용자 정보 요청 중에 오류가 발생하였습니다.{}", e.getMessage());
}
// [STEP4] 사용자 정보가 존재한다면 값을 불러와서 OAuth2KakaoUserInfoDto 객체로 구성하여 반환합니다.
if (responseUserInfo != null && responseUserInfo.getBody() != null && responseUserInfo.getStatusCode().is2xxSuccessful()) {
Map<string, object=""> body = responseUserInfo.getBody();
if (body != null) {
Map<string, object=""> kakaoAccount = this.cvtObjectToMap(body.get("kakao_account"));
Map<string, object=""> profile = this.cvtObjectToMap(this.cvtObjectToMap(body.get("kakao_account")).get("profile"));
resultDto = OAuth2KakaoUserInfoDto.builder()
.id(body.get("id").toString()) // 사용자 아이디 번호
.statusCode(responseUserInfo.getStatusCode().value()) // 상태 코드
.email(kakaoAccount.get("email").toString()) // 이메일
.profileImageUrl(profile.get("profile_image_url").toString())
.thumbnailImageUrl(profile.get("thumbnail_image_url").toString())
.nickname(profile.get("nickname").toString())
.name(profile.get("name").toString())
.ageRange(profile.get("age_range").toString())
.birthday(profile.get("birthday").toString())
.gender(profile.get("gender").toString())
.build();
log.debug("최종 구성 결과 :: {}", resultDto);
}
}
return resultDto;
}
}
💡 아래와 같은 데이터 구조를 불러옴을 확인하였습니다.
<200 OK OK,
{
id=3785024526,
connected_at=2024-11-09T07:44:14Z,
kakao_account={
profile_nickname_needs_agreement=false,
profile_image_needs_agreement=false,
profile={
nickname=이종훈,
thumbnail_image_url=http://k.kakaocdn.net/dn/JIQ7w/xxxxxx/j8e0bYvenB7AEIknDSZHmk/img_110x110.jpg,
profile_image_url=http://k.kakaocdn.net/dn/JIQ7w/xxxxx/j8e0bYvenB7AEIknDSZHmk/img_640x640.jpg,
is_default_image=false,
is_default_nickname=false
},
name_needs_agreement=false,
name=이종훈,
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=adjh54@kakao.com,
has_age_range=true,
age_range_needs_agreement=false,
age_range=30~39,
has_birthday=true,
birthday_needs_agreement=false,
birthday=0501,
birthday_type=SOLAR,
has_gender=true,
gender_needs_agreement=false,
gender=male
}
}
}
💡 [참고] 아래와 같이 호출을 하였습니다.
Map<String, Object> kakaoAccount = this.cvtObjectToMap(body.get("kakao_account"));
Map<String, Object> profile = this.cvtObjectToMap(kakaoAccount.get("profile"));
System.out.println("Profile :: " + profile);
System.out.println("ID: " + body.get("id"));
System.out.println("Status Code: " + responseUserInfo.getStatusCode().value());
System.out.println("Profile Image URL: " + profile.get("profile_image_url"));
System.out.println("Thumbnail Image URL: " + profile.get("thumbnail_image_url"));
System.out.println("Email: " + kakaoAccount.get("email"));
System.out.println("Nickname: " + profile.get("nickname"));
System.out.println("Name: " + kakaoAccount.get("name"));
System.out.println("Age Range: " + kakaoAccount.get("age_range"));
System.out.println("Birthday: " + kakaoAccount.get("birthday"));
System.out.println("Gender: " + kakaoAccount.get("gender"));
4. 최종 결과 확인
💡 최종 결과 확인
- 아래와 같은 추가적인 결과를 반환받음을 확인하였습니다.
오늘도 감사합니다 😀
반응형