Framework/Spring

[Spring] 카카오 로그인 기능 추가하기

IT수정 2024. 12. 23. 16:18

Service

MemberService.java

package code.code_api.service;

import code.code_api.domain.CodeMember;
import code.code_api.dto.MemberDTO;
import code.code_api.dto.MemberModifyDTO;

import java.util.stream.Collectors;

public interface MemberService {

    MemberDTO getKakaoMember(String accessToken);

    void modifyMember(MemberModifyDTO memberModifyDTO);

    //엔터티 > DTO 변환
    default MemberDTO entityToDTO(CodeMember member){
        MemberDTO memberDTO = new MemberDTO(
                member.getEmail(),
                member.getNickname(),
                member.getNickname(),
                member.isSocial(),
                member.getMemberRoleList().stream().map(memberRole ->
                        memberRole.name()).collect(Collectors.toList())
        );
        return memberDTO;
    }
}

 

MemberServiceImpl.java

package code.code_api.service;

import code.code_api.domain.CodeMember;
import code.code_api.domain.MemberRole;
import code.code_api.dto.KakaoUserInfoDTO;
import code.code_api.dto.MemberDTO;
import code.code_api.dto.MemberModifyDTO;
import code.code_api.repository.CodeMemberRepository;
import lombok.RequiredArgsConstructor;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.LinkedHashMap;
import java.util.Optional;

@Service
@Slf4j
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{

    private final CodeMemberRepository codeMemberRepository;

    private final PasswordEncoder passwordEncoder;

    //accessToken을 이용해서 사용자 정보 가져오기 => 기존 회원 정보가 있는 경우, 없는 경우
    @Override
    public MemberDTO getKakaoMember(String accessToken) {
        KakaoUserInfoDTO kakaoUserInfo = getNicknameFromKakaoAccessToken(accessToken);
        log.info("가져온 nickName {}", kakaoUserInfo);
        Optional<CodeMember> result = codeMemberRepository.findByNickname(kakaoUserInfo.getNickname());

        //기존 회원의 경우 DTO로 변환한 뒤 반환
        if(result.isPresent()){
            MemberDTO memberDTO = entityToDTO(result.get());
            return memberDTO;
        }

        //새로운 회원의 경우 비밀번호 임의로 생성
        CodeMember socialMember = makeSocialMember(kakaoUserInfo.getId(), kakaoUserInfo.getNickname());
        codeMemberRepository.save(socialMember);
        MemberDTO memberDTO = entityToDTO(socialMember);
        return memberDTO;
    }

    private KakaoUserInfoDTO getNicknameFromKakaoAccessToken(String accessToken) {

        String kakaoGetUserURL = "https://kapi.kakao.com/v2/user/me";

        //토큰이 없을 경우
        if(accessToken == null) {
            throw new RuntimeException("AccessToken is null");
        }

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
        HttpEntity<Object> entity = new HttpEntity<>(headers);

        //실제로 보내야 한다. UriComponentsBuilder를 사용한다.
        UriComponents uriBuilder = UriComponentsBuilder.fromHttpUrl(kakaoGetUserURL).build();

        //이 때 나오는 정보가 LinkedHashMap 형태로 나온다.
        ResponseEntity<LinkedHashMap> response = restTemplate.exchange(
                uriBuilder.toString(),
                HttpMethod.GET,
                entity,
                LinkedHashMap.class
        );

        log.info("response {}", response);

        //사용자 정보에서 아이디를 가져오자, nickName, id
        LinkedHashMap<String, LinkedHashMap> bodyMap = response.getBody();
        log.info("bodyMap {}", bodyMap);
        log.info("id {}", bodyMap.get("id"));
        String id = String.valueOf(bodyMap.get("id"));

        LinkedHashMap<String, String> properties = bodyMap.get("properties");
        log.info("nickName {}", properties.get("nickname"));
        String nickname = properties.get("nickname");

        return new KakaoUserInfoDTO(id, nickname);
    }

    //해당 닉네임을 가진 회원이 없다면 새로운 회원을 추가할 때 패스워드를 임의로 생성한다.
    private String makeTempPassword() {
        StringBuffer buffer = new StringBuffer();

        for(int i = 0; i < 10; i++) {
            buffer.append( (char) ( (int)(Math.random() * 55) + 65 ));
        }
        return buffer.toString();
    }


    //소셜 회원 만들기
    private CodeMember makeSocialMember(String id, String nickname){
        String tempPassword = makeTempPassword();
        log.info("tempPassword: ", tempPassword);

        //회원만들기
        CodeMember member = CodeMember.builder()
                .email(id + "@email.com")
                .pw(passwordEncoder.encode(tempPassword))
                .nickname(nickname)
                .social(true)
                .build();
        member.addRole(MemberRole.USER);
        return member;
    }

    //회원 수정
    @Override
    public void modifyMember(MemberModifyDTO memberModifyDTO) {

        Optional<CodeMember> result = codeMemberRepository.findByNickname(memberModifyDTO.getNickName());

        CodeMember member = result.orElseThrow();
        member.setPw(passwordEncoder.encode(memberModifyDTO.getPw()));
        member.setSocial(false);
        member.setNickname(memberModifyDTO.getNickName());
        codeMemberRepository.save(member);
    }
}

 

Controller

SocialController.java

package code.code_api.controller;

import code.code_api.dto.MemberDTO;
import code.code_api.dto.MemberModifyDTO;
import code.code_api.service.MemberService;
import code.code_api.util.JWTUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@Slf4j
@RequiredArgsConstructor
public class SocialController {

    private final MemberService memberService;

    @GetMapping("/api/member/kakao")
    public Map<String, Object> getMemberKakao(@RequestParam("accessToken") String accessToken) {
        log.info("react에서 가져온 accessToken {}", accessToken);

        MemberDTO memberDTO = memberService.getKakaoMember(accessToken);
        Map<String, Object> claims = memberDTO.getClaims();

        String jwtAccessToken = JWTUtil.generateToken(claims, 10);
        String jwtRefreshToken = JWTUtil.generateToken(claims, 60 * 24);

        claims.put("accessToken", jwtAccessToken);
        claims.put("refreshToken", jwtRefreshToken);

        return claims;
    }

    @PutMapping("/api/member/modify")
    public Map<String, String> modify(@RequestBody MemberModifyDTO memberModifyDTO) {
        memberService.modifyMember(memberModifyDTO);
        return Map.of("RESULT", "modified");
    }
}

 

DTO

KakaoUserInfoDTO.java

package code.code_api.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter @Setter
@AllArgsConstructor
@NoArgsConstructor
public class KakaoUserInfoDTO {

    private String id;

    private String nickname;
}

 

MemberModifyDTO.java

package code.code_api.dto;

import lombok.Data;

@Data
public class MemberModifyDTO {

    private String email;

    private String pw;

    private String nickName;
}

 

Repository

CodeMemberRepository.java

package code.code_api.repository;

import code.code_api.domain.CodeMember;
import org.aspectj.apache.bcel.classfile.Code;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface CodeMemberRepository extends JpaRepository<CodeMember, String> {

    @EntityGraph(attributePaths = {"memberRoleList"})
    @Query("select m from CodeMember m where m.email = :email")
    CodeMember getWithRoles(@Param("email") String email);

    //nickname으로 찾기
    Optional<CodeMember> findByNickname(String nickname);
}

 

Test

CodeMemberRepositoryTest.java

@Test
    void 회원삭제() {
        String email = "3846973570@email.com";

        CodeMember member = codeMemberRepository.getWithRoles(email);
        if(member == null){
            log.info("회원이 존재하지 않습니다. 이메일 : {}", email);
            return;
        }

        //회원 삭제
        codeMemberRepository.delete(member);
        log.info("회원 삭제 완료 {}", email);
    }

 

'Framework > Spring' 카테고리의 다른 글

[Spring] 시큐리티 로그인 로직  (2) 2024.12.18
[Spring] 코드 프로젝트  (0) 2024.12.05
[Spring] 시큐어 코딩 예제  (0) 2024.11.22
[Spring] 문의 페이지 테스트  (0) 2024.11.13
[Spring] 에러 페이지 예제  (0) 2024.11.12