공부메모 & 오류해결/Spring Boot

[Spring Boot + OAUTH2.0] Spring Boot 소셜로그인(카카오) 구현법(Oauth2.0)

남건욱 2023. 10. 10. 13:03

목차

    반응형
    oauth2.0

    사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당 정보를 사용할 수 있도록 한다.

    - 자주 사용하지 않는 웹사이트에 개인정보를 입력해서 회원가입을 해야 하나? 할 때 간단하게 소셜 로그인을 사용해서 이용할 수 있다.

     

     

    카카오 소셜로그인을 위한 준비가 안되었다면 아래 링크를 통해 설정한 뒤 본 게시글을 따라 해야 한다.

    카카오 소셜로그인을 위한 설정

     

    카카오 소셜로그인을 위한 설정(Spring + Oauth2.0)

    1. 카카오 개발자 사이트 들어가기 https://developers.kakao.com/console/app 카카오계정 accounts.kakao.com 위 링크로 들어가 준다. 위와 같은 화면이 뜨면 된다. 2. 카카오 애플리케이션 추가 파란색버튼인 애

    ngwdeveloper.tistory.com

     

     

    1. Dto 설정

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class OauthUserDto {
        private String id;
        private String nickname;
        private String email;
    
    }
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class OauthTokenDto {
        private String accessToken;
    }

    OauthTokenDto - 엑세스 토큰을 저장할 Dto

    OauthUserDto - 엑세스 토큰을 이용해서 사용자의 정보를 저장할 Dto

     

     

     

     

     

    2. 엔티티 설정

        public User(OauthUserDto userDto, String password, UserRoleEnum role){
            this.username = userDto.getEmail();
            this.password = password;
            this.nickname = userDto.getNickname();
            this.introduction = "-";
            this.role = role;
        }
    
        public User kakaoIdUpdate(String kakaoId){
            this.kakaoId = kakaoId;
            return this;
        }

    이부분은 본인의 User객체의 저장하는 목록에 따라 다를 수 있다.

    나는 유저이름, 닉네임을 토큰을 이용해서 가져왔다. 또한 password, introduction, role만 설정해 주었다.

     

    kakaoIdUpdate 메서드는 내가 설정해 둔 칼럼에 값을 추가할 수 있도록 만들어뒀다.

     

     

     

     

    3. 레포지토리 설정

    Optional<User> findByKakaoId(String kakaoId);

    중복가입을 체크하기 위해 UserRepository에 kakaoId로 유저를 검색할 수 있도록 Optional형식으로 코드를 추가해 줬다.

     

     

     

     

    4. 통합 서비스 설정(카카오, 네이버, 구글)

    public interface OauthService {
    
        OauthTokenDto socialLogin(String code) throws JsonProcessingException, UnsupportedEncodingException;
    
    
        String getToken(String code) throws JsonProcessingException, UnsupportedEncodingException;
    
    
        OauthUserDto getUserInfo(String accessToken) throws JsonProcessingException;
    
        User registerUserIfNeeded(OauthUserDto oauthTokenDto);
    
    }

    interface형식으로 만들어뒀다. 소셜 로그인(카카오, 네이버, 구글)을 할 때 각각 오버라이딩 해서 사용할 것이기 때문에 추가해 줬다.

     

     

     

     

     

    5. 카카오 서비스 설정

    나는 application.properties를 사용하기 때문에 내부에 클라이언트 id, 리다이렉트 url을 미리 넣어줬다.

     

    @Service
    @RequiredArgsConstructor
    public class KakaoService implements OauthService{
        private final UserRepository userRepository;
        private final PasswordEncoder passwordEncoder;
        private final RestTemplate restTemplate;
        private final JwtUtil jwtUtil;
    
        @Value("${kakao.client.id}")
        private String CLIENT_ID;
    
        @Value("${kakao.redirect.url}")
        private String REDIRECT_URL;
    
        @Override
        public OauthTokenDto socialLogin(String code) throws JsonProcessingException{
            // 1. "인가 코드"로 "액세스 토큰" 요청
            String accessToken = getToken(code);
    
            // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
            OauthUserDto oauthUserDto = getUserInfo(accessToken);
    
            // 3. 필요시에 회원가입
            User kakaoUser = registerUserIfNeeded(oauthUserDto);
    
            // 4. JWT 토큰 생성
            String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
    
            return new OauthTokenDto(createToken);
        }
    
        @Override
        public String getToken(String code) throws JsonProcessingException{
            // 요청 URL 만들기
            URI uri = UriComponentsBuilder
                    .fromUriString("https://kauth.kakao.com")
                    .path("/oauth/token")
                    .encode()
                    .build()
                    .toUri();
    
            // HTTP Header 생성
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-type","application/x-www-form-urlencoded;charset=utf-8");
    
            // HTTP Body 생성
            MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
            body.add("grant_type", "authorization_code");
            body.add("client_id", CLIENT_ID); // 자신의 REST API 키
            body.add("redirect_uri", REDIRECT_URL);
            body.add("code",code); // 인가 코드
    
            // 방법 1
            ResponseEntity<String> response = restTemplate.postForEntity(uri,new HttpEntity<>(body, headers),String.class);
    
    
            // HTTP 응답 (JSON) -> 액세스 토큰 값을 반환합니다.
            JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
            return jsonNode.get("access_token").asText();
        }
    
        @Override
        public OauthUserDto getUserInfo(String accessToken) throws  JsonProcessingException{
            // 요청 URL 만들기
            URI uri = UriComponentsBuilder
                    .fromUriString("https://kapi.kakao.com")
                    .path("/v2/user/me")
                    .encode()
                    .build()
                    .toUri();
    
            // HTTP Header 생성
            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Bearer " + accessToken);
            headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
    
    
            // Http 요청 보내기
            RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
                    .post(uri)
                    .headers(headers)
                    .body(new LinkedMultiValueMap<>());
    
            // HTTP 요청 보내기
            ResponseEntity<String> response = restTemplate.exchange(
                    requestEntity,
                    String.class
            );
    
            JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
            String id = jsonNode.get("id").asText();
            String nickname = jsonNode.get("properties")
                    .get("nickname").asText();
            String email = jsonNode.get("kakao_account")
                    .get("email").asText();
    
            return new OauthUserDto(id, nickname, email);
        }
    
        @Override
        public User registerUserIfNeeded(OauthUserDto userDto){
            // DB 에 중복된 Kakao Id 가 있는지 확인
            String kakaoId = userDto.getId();
            User kakaoUser = (User) userRepository.findByKakaoId(kakaoId).orElse(null);
    
            if (kakaoUser == null) {
                // 카카오 사용자 email 과 동일한 id 를 가진 회원이 있는지 확인
                String kakaoEmail = userDto.getEmail();
                User sameEmailUser = userRepository.findByUsername(kakaoEmail).orElse(null);
                if (sameEmailUser != null) {
                    kakaoUser = sameEmailUser;
                    // 기존 회원정보에 카카오 Id 추가
                    kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
                } else {
                    // 신규 회원가입
                    // password: random UUID
                    String password = UUID.randomUUID().toString();
                    String encodedPassword = passwordEncoder.encode(password);
    
                    kakaoUser = new User(userDto, encodedPassword, UserRoleEnum.USER);
                    kakaoUser.kakaoIdUpdate(kakaoId);
                }
                userRepository.save(kakaoUser);
            }
            return kakaoUser;
        }
    
    }

    socialLogin - 소셜 로그인 메서드. 인가 코드를 넘겨주고 JWT 토큰을 반환받는다.

    getToken - 인가 코드를 사용한 액세스 토큰 요청 메서드. 전달받은 인가 코드를 넘겨주고 액세스 토큰을 반환받는다.

    getUserInfo - 인가 토큰을 통해 사용자 정보를 가져오는 메서드. 액세스 토큰을 넘겨주고 회원정보를 반환받는다.

    registerUserIfNeeded - 카카오 ID 정보로 회원가입을 해주는 메서드. 회원정보를 넘겨주고 User객체를 추가한다.

     

     

     

     

    6. 컨트롤러 설정

    @Controller
    @RequiredArgsConstructor
    @RequestMapping("/api")
    public class OauthLoginController {
        private final KakaoService kakaoService;
        private final JwtUtil jwtUtil;
    
        @GetMapping("/user/kakao/callback")
        public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException{
            OauthTokenDto tokenDto = kakaoService.socialLogin(code);
            jwtUtil.addJwtToCookie(tokenDto.getAccessToken(), response);
            return "redirect:/";
        }
    
    }

    GET매핑으로 받아올 것이고 주소는 설정해 둔 리다이렉션 주소와 일치하게 설정하면 된다.

    매개변수는 RequestParam 형식으로 code를 받아오고, HttpServletResponse로 응답을 받아온다.

    OauthTokenDto는 액세스토큰 정보를 가지고 있는 Dto이다.

    여기에 kakaoService클래스의 socialLogin 메서드에 code값을 넣어준 뒤 액세스 토큰을 받아온다.

    그 뒤 jwtUtil의 addJwtToCookie 메서드를 호출하여 JWT 토큰을 생성하고 응답의 쿠키에 추가한다.

    넘겨주는 값은 액세스토큰값, 응답값이다.

     

     

    7. 테스트 방법

    https://kauth.kakao.com/oauth/authorize?
    client_id=본인의 client ID
    &redirect_uri=본인의 redirect URI
    &response_type=code

    위 코드를 넣어서 테스트해 보면 된다. 

     

    나는 버튼을 하나 만들어서 테스트했다.

     

    <추가>

    네이버 소셜로그인 구현하기

     

    Spring Boot 소셜로그인(네이버) 구현법(Oauth2.0)

    Oauth2.0 사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당

    ngwdeveloper.tistory.com

     

    구글 소셜로그인 구현하기

     

    Spring Boot 소셜로그인(구글) 구현법(Oauth2.0)

    Oauth2.0 사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당

    ngwdeveloper.tistory.com

     

    반응형
    프로필사진

    남건욱's 공부기록