메인 프로젝트를 하면서 REST API로 카카오 로그인을 구현해보았다.
해당 부분을 기억하기 위해 이 글을 작성하였다.
이 포스팅은 프로젝트 과정에서 흐름을 기억하기 위해 작성한 것으로, 기본 흐름의 틀정도라고 생각하면 좋을 듯 하다.
해당 포스팅의 코드는 아래의 깃헙 주소에서도 확인 가능.
❗️본 포스팅은 이전 포스팅에서 이어집니다❗️
GitHub - Bhinney/Study: ✨ 공부하면서 기록하는 공간 ✨
✨ 공부하면서 기록하는 공간 ✨. Contribute to Bhinney/Study development by creating an account on GitHub.
github.com
✨ 들어가기 전에, 메인 프로젝트에서 사용한 카카오 로그인의 흐름 ✨
: 아래의 흐름을 바탕으로 프로젝트의 코드에 약간의 변형을 주어 구현할 예정
📚 자료 참조 : https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
📎 구현 전에 준비
- ⭐️ Kakao Developers에서 애플리케이션 설정하기 ⭐️
- https://developers.kakao.com/
- 위의 사이트로 들어가 애플리케이션을 생성해준다.
- 요약 정보에서 앱 키를 확인할 수 있다.
- 제품 설정 👉🏻 카카오 로그인 👉🏻 Redirect Uri 등록
- 제품 설정 👉🏻 동의 항목 👉🏻 필요한 정보 동의 설정 (필자는 이메일과 이름을 가져옴)
1️⃣ application.yml 수정
- security 설정 추가
spring:
...
security:
oauth2:
client:
registration:
kakao:
clientId: ${KAKAO_CLIENT_ID}
clientAuthenticationMethod: post
authorizationGrantType: authorization_code
redirectUri: http://localhost:8080/login/oauth2/code/kakao
scope:
- profile_nickname
- account_email
clientName: Kakao
provider:
kakao:
authorizationUri: https://kauth.kakao.com/oauth/authorize
tokenUri: https://kauth.kakao.com/oauth/token
userInfoUri: https://kapi.kakao.com/v2/user/me
userNameAttribute: id
...
2️⃣ build.gradle 추가
- oauth2 관련 의존성 추가
...
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
...
3️⃣ KakaoToken 클래스 생성
- Kakao 응답 정보를 받아오기 위한 클래스
@Getter
@ToString
public class KakaoToken {
private String access_token;
private String refresh_token;
private String token_type;
private int expires_in;
private String scope;
private int refresh_token_expires_in;
}
4️⃣ KakaoProfile 클래스 생성
- Kakao 회원 정보를 받아올 클래스
@Data
public class KakaoProfile {
public String id; /* 소셜 아이디로 들어갈 에정 */
public String connected_at;
public Properties properties;
public KakaoAccount kakao_account;
public class Properties {
public String nickname;
}
@Data
public class KakaoAccount {
public Boolean profile_nickname_needs_agreement;
public Boolean profile_image_needs_agreement;
public Profile profile;
public Boolean has_email;
public Boolean email_needs_agreement;
public Boolean is_email_valid;
public Boolean is_email_verified;
public String email;
@Data
public class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url;
public Boolean is_default_image;
}
}
}
5️⃣ KakaoUserInfo, OAuth2UserInfo, OAuth2UserInfoFactory 클래스 생성
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public Map<String, Object> getAttributes() {
return attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
}
public class KakaoUserInfo extends OAuth2UserInfo{
public KakaoUserInfo(Map<String, Object> attributes) {
super(attributes);
}
/* 소셜 아이디 */
@Override
public String getId() {
return attributes.get("id").toString();
}
/* 회원 이름 */
@Override
public String getName() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
if (properties == null) {
return null;
}
return (String) properties.get("nickname");
}
/* 회원 이메일 */
@Override
public String getEmail() {
return (String) attributes.get("account_email");
}
}
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(ProviderType providerType, Map<String, Object> attributes) {
switch (providerType) {
case KAKAO: return new KakaoUserInfo(attributes);
default: throw new IllegalArgumentException("Invalid Provider Type.");
}
}
}
6️⃣ KakaoService 클래스 생성
- Kakao 토큰 받기
- Kakao 사용자 저장 및 정보 가져오기
- 첫 이용자는 강제 회원가입
- 소셜 사용자는 SOCIAL 권한 부여 -> 이후 단 한 번만 권한 수정 가능 (Client or Seller)
@Service
@RequiredArgsConstructor
public class KakaoService {
private final MemberRepository memberRepository;
@Value("${spring.security.oauth2.client.registration.kakao.clientId}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.kakao.redirectUri}")
private String redirectUri;
@Value("${spring.security.oauth2.client.provider.kakao.tokenUri}")
private String tokenUri;
@Value("${spring.security.oauth2.client.provider.kakao.userInfoUri}")
private String userInfoUri;
/* code 로 kakao 의 엑세스 토큰 발급 받기 */
public KakaoToken getAccessToken(String code) {
/* RequestParam */
MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
param.add("grant_type", "authorization_code");
param.add("client_id", clientId);
param.add("redirect_uri", redirectUri);
param.add("code", code);
/* 요청 */
HttpHeaders headers = new HttpHeaders();
headers.set("Content-type","application/x-www-form-urlencoded;charset=utf-8");
HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(param, headers);
RestTemplate template = new RestTemplate();
KakaoToken kakaoToken = template.postForObject(tokenUri, requestEntity, KakaoToken.class);
return kakaoToken;
}
/* 받은 엑세스 토큰으로 카카오 회원 정보 가져오기 */
public KakaoProfile findProfile(String token) {
RestTemplate template = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
HttpEntity<MultiValueMap<String, String> > requestEntity = new HttpEntity<>(null, headers);
KakaoProfile kakaoProfile = template.postForObject(userInfoUri, requestEntity, KakaoProfile.class);
return kakaoProfile;
}
/* 카카오 로그인 */
@Transactional
public Member saveMember(String accessToken) {
/* 사용자 정보 받아오기 */
KakaoProfile profile = findProfile(accessToken);
Member member = memberRepository.findMemberByEmail(profile.getKakao_account().getEmail());
/* 첫 이용자 강제 회원가입 */
if (member == null) {
String name = profile.getKakao_account().getProfile().getNickname();
if (name == null || name.equals("")) {
name = "이름을 입력하세요";
}
member = new Member(
name,
profile.getKakao_account().getEmail(),
profile.getId(),
"010-0000-0000",
"주소를 입력하세요."
);
member.setCreateMember(
"카카오 로그인 사용자",
"SOCIAL",
List.of("SOCIAL"),
ProviderType.KAKAO
);
memberRepository.save(member);
}
/* 만일 사용자가 로컬 사용자라면 예외를 던져야 함 */
if (member.getProviderType() == ProviderType.LOCAL) {
throw new RuntimeException("소셜 로그인 사용자가 아닙니다.");
}
return member;
}
}
6️⃣ MemberService에 카카오 로그인 추가
- UsernamePasswordAuthenticationToken 를 쓰지 않으므로 새로운 함수 추가
@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final CustomAuthorityUtils authorityUtils;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final JwtProvider jwtProvider;
private final RedisTemplate<String, Object> redisTemplate;
...
/* 카카오 로그인 */
public TokenDto kakaoLogin(LoginRequestDto requestDto) {
TokenDto tokenDto = jwtProvider.generatedTokenDto(requestDto.getEmail());
/* Refresh Token 저장 */
redisTemplate.opsForValue()
.set("RefreshToken:" + requestDto.getEmail(), tokenDto.getRefreshToken(),
tokenDto.getRefreshTokenExpiresIn() - new Date().getTime(), TimeUnit.MICROSECONDS);
return tokenDto;
}
...
}
6️⃣ MemberDto, MemberMapper에 소셜 응답추가
- 소셜 권한 응답을 위해 DTO와 Mapper에 소셜 응답 추가
public class MemberDto {
...
@Getter
@Builder
@AllArgsConstructor
public static class SocialResponseDto{
private long memberId;
private String email;
private String name;
private String phone;
private String address;
private String role;
private String accessToken;
}
}
@Mapper(componentModel = "spring")
public interface MemberMapper {
...
default MemberDto.SocialResponseDto memberToSocialResponseDto(Member member, String token) {
if (member == null && token == null) {
return null;
}
MemberDto.SocialResponseDto response =
MemberDto.SocialResponseDto.builder()
.memberId(member.getMemberId())
.email(member.getEmail())
.name(member.getName())
.phone(member.getPhone())
.address(member.getAddress())
.role(member.getRole())
.accessToken(token)
.build();
return response;
}
}
7️⃣ MemberController에 카카오 로그인 추가
- 리다이렉트로 들어오는 주소로 GetMapping
- 해당 코드로 KakaoService에서 토큰 받고, 정보 가져오기
- 서버에서 로그인 하여 서버의 토큰 발급
@Slf4j
@RestController
@RequiredArgsConstructor
@Transactional
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
private final KakaoService kakaoService;
...
/* 카카오 로그인 */
/* frontend 로 부터 받은 인가 코드 받기 및 사용자 정보 받기, 회원가입 */
@GetMapping("/login/oauth2/code/kakao")
public ResponseEntity kakaoLogin(@RequestParam("code") String code) {
/* access 토큰 받기 */
KakaoToken oauthToken = kakaoService.getAccessToken(code);
/* 사용자 정보받기 및 회원가입 */
Member member = kakaoService.saveMember(oauthToken.getAccess_token());
/* jwt 토큰 저장 */
LoginRequestDto requestDto = new LoginRequestDto(member.getEmail(), member.getPassword());
TokenDto tokenDto = memberService.kakaoLogin(requestDto);
/* 엑세스 토큰 헤더에 담아주기 */
HttpHeaders httpHeaders = setHeader(tokenDto.getAccessToken());
/* 역할에 따라 응답 바디가 다르므로, 나누어 주었다.*/
if(member.getRole().equals("SELLER")) {
return new ResponseEntity<>(mapper.memberToSellerResponseDto(member), httpHeaders, HttpStatus.OK);
} else if (member.getRole().equals("CLIENT")) {
return new ResponseEntity<>(mapper.memberToClientResponseDto(member), httpHeaders, HttpStatus.OK);
} else if (member.getRole().equals("SOCIAL")) {
return new ResponseEntity<>(mapper.memberToSocialResponseDto(member, tokenDto.getAccessToken()),
httpHeaders, HttpStatus.OK);
}
/* 로그인이 실패할 경우, 문제가 존재하는 것 */
throw new RuntimeException("로그인에 실패하였습니다.");
}
/* 로그인 헤더 설정 */
private HttpHeaders setHeader(String token) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Authorization", token);
return httpHeaders;
}
}
'Spring' 카테고리의 다른 글
[Spring] REST Docs (0) | 2023.01.04 |
---|---|
[Spring] 로그인 구현 번외 - 소셜 로그인 시 권한 부여 (0) | 2022.12.20 |
[Spring] 로그인 구현 2 - 회원 가입 & 자체 로그인 구현 (1) | 2022.12.20 |
[Spring] 로그인 구현 1 - 회원 엔티티, Mapper, Repository 구현 (0) | 2022.12.20 |
[Spring] Transaction (0) | 2022.12.19 |
댓글