게시판 + 카카오 로그인 API

박세건·2023년 8월 9일
0

기존에 만들어봤던 게시판에 카카오 로그인 API 기능을 연결해서 사용해보자
메타코딩의 스프링부트 강좌를 참고
메타코딩

OAuth 2.0 개념

원래는 각각의 사이트마다 개인정보를 등록해서 사이트를 사용합니다.
하지만 이렇게 개인정보를 계속 등록하게되면 점점 더 많은 사이트에 내 개인정보가 퍼지게 되고
결론적으로 개인정보를 안전하게 관리할 수가 없게됩니다.

이를 해결하기 위해서 나타난게 OAuth 2.0 입니다.
OAuth 2.0 이란, Open Auth 로 국가에서 지정해주는 대형포털 사이트들에게 개인정보를 대신해서 관리 해주는 것입니다. 예를 들어서 카카오와 네이버 등등 이있습니다.
간단하게 말해서 인증처리를 이런 대형포털들이 대신 해주는 것입니다.
이렇게 하게되면 개인정보를 이런 대형 포털에서만 관리할 수있게 해서 편리함과 안전함을 챙길 수 있습니다.

간단한 구조를 설명하면
홍길동이라는 사람과 내가만든 블로그서버카카오 API 서버와 이 카카오에서 관리 하는 자원 서버 가 있다고 하자
1. 홍길동은 블로그 서버에 로그인 요청을 합니다.
2. 블로그 서버는 요청에대한 로그인 화면을 응답해준다.
3. 홍길동은 카카오 로그인을 선택한다.
4. 그러면 카카오 API 서버로 요청이 가게되고 이를 카카오가 로그인창을 응답해준다.
5. 홍길동이 카카오로 로그인을 합니다.
6. 이 결과를 카카오가 받게되고 이를 블로그 서버에게 안전한 로그인이라고 알려준다.
7. 블로그 서버는 이때 CODE를 받게되는데 이 코드를 갖고 카카오 서버에 요청하면
8. 카카오 서버는 블로그 서버에게 Access Token 을 전달하게됩니다.

이 Access Token으로 블로그 서버는 카카오의 자원서버로부터 홍길동의 정보를 가져올 수 있는 권한을 갖게됩니다.

관계

홍길동 - 리소스 오너 (홍길동 정보에 접근할 수 있는 주인)
블로그 서버 - 클라이언트(카카오의 입장에서)
카카오API서버 - 인증 서버(OAuth 서버)
자원 서버 - 리소스 서버(정보를 갖고 있는 서버)

- 추가적으로 spring에서 공식적으로 제공해주는 OAuth는 페이스북과 구글이 있다.

인증 코드 받기

카카오 Developers 에서 내 애플리케이션을 만들어준다
(만드는 과정은 위에 올려둔 메타코딩 방법을 따라하자)
카카오 API를 할때에는 영상에서 알려주는 것처럼 주어지는 정보를 메모장에 적어두고 진행하는 것이 편리했다.


Developers에 나오는 요청 코드에 나의 REST API KEY 랑 redirect url 을 넣어준다.
주소를 내 로그인 html에 추가

  <form action="/auth/loginProc" method="post">
            <div class="form-group">
                <label for="loginId"> 아이디 : </label>
                <input type="text" class="form-control" id="loginId" placeholder="Enter ID" name="username">
            </div>
            <div class="form-group">
                <label for="password"> 비밀번호 : </label>
                <input type="password" class="form-control" id="password" placeholder="Enter password" name="password">
            </div>
            <div th:if="${failLogin}">
                <p id="valid" class="alert alert-danger">로그인에 실패하였습니다. 다시 시도해주세요!</p>
            </div>
            </br></br>
            <button type="submit" class="btn btn-primary">로그인</button>
            //추가된 카카오 로그인 API
            <a href="https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=20c66978b03bcfb37f3f31f7a06a2488&redirect_uri=http://localhost:8080/auth/kakao/callback">
                <img height="36px" src="/image/kakao_login_button.png">
            </a>
        </form>

로그인 버튼을 클릭하면 카카오 로그인 창이 뜨고 로그인을 완료하면

http://localhost:8080/auth/kakao/callback?code=4gh4hH2fmyfKqV_Jy9tDhlANZLghKfZt1q3awnx_g3lkMz9eR0_16lYt8kzhBgYZKfSdeworDSAAAAGJzurJGw

해당 주소로 넘어가게된다. 우리가 설정한 콜백주소이고 code 값을 전달 해준다.
code 값을 통해서 access token을 부여받을 것이다.
개인정보에 접근하기위해서는 token이 필요하기 때문이다.

토큰 받기

인가 코드로 토큰 발급을 요청합니다. 인가 코드 받기만으로는 카카오 로그인이 완료되지 않으며, 토큰 받기까지 마쳐야 카카오 로그인을 정상적으로 완료할 수 있습니다.
따라서 토큰까지 받아보자

토큰 발급 요청 주소

https://kauth.kakao.com/oauth/token

이 주소에 http body에 데이터를 넣어서 전달해라 이때 데이터를 전달하기 위해서 RestTemplate 클래스를 사용한다.

RestTemplate란,

Spring의 HTTP 통신 템플릿
Spring에서 지원하는 객체로 간편하게 Rest 방식 API를 호출할 수 있는 Spring 내장 클래스이다. Spring 3.0부터 지원되었고, json, xml 응답을 모두 받을 수 있다.
Rest API 서비스를 요청 후 응답받을 수 있도록 설계되어 있으며 HTTP 프로토콜의 메소드(ex. GET, POST, DELETE, PUT)들에 적합한 여러 메소드를 제공한다.
추가 내용

Rest 방식 API 란, Rest 란,

  • REST란?
    REST(Representational State Transfer)의 약자로 자원을 이름으로 구분하여 해당 자원의 상태를 주고받는 모든 것을 의미합니다.
    (추가)
    HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고,
    HTTP Method(POST, GET, PUT, DELETE, PATCH 등)를 통해 작업을 수행하는 것
  • 그렇다면 REST API란?
    REST의 원리를 따르는 API를 의미합니다.

REST API 원리

  1. URI는 동사보다는 명사를, 대문자보다는 소문자를 사용하여야 한다.
  2. 마지막에 슬래시 (/)를 포함하지 않는다.
  3. 언더바 대신 하이폰(-)을 사용한다.
  4. 파일확장자는 URI에 포함하지 않는다.
  5. 행위를 포함하지 않는다.

내가 공부한 블로그


REST를 공부하던중 좋은 URI 관련 좋은 사진이 있어서 첨부한다.

이어서 http헤더와 http바디를 설정해주고 HttpEntity에 저장시켜준후에 RestTemplate.exchange() 함수를 사용해서 요청을 보내고 응답을 받아온다.

        RestTemplate restTemplate = new RestTemplate();
        //HttpHeader 오브젝트 생성
        HttpHeaders httpHeaders = new HttpHeaders();
        //Content-type 을 HttpHeader에 담는다는 것은 내가 담을 데이터가 key-value 데이터라고 알려주는 것이다
        httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        //httpBody 생성 부분
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", "20c66978b03bcfb37f3f31f7a06a2488");
        params.add("redirect_uri", "http://localhost:8080/auth/kakao/callback");
        params.add("code=", code);
        //HttpHeader와 Httpbody를 하나의 오브젝트에 담김
        //restTemplate.exchange() 함수가 HttpEntity를 파라미터로 받기 때문
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
                new HttpEntity<>(params, httpHeaders);
        //Http 요청하기 - POST방식 그리고 response 변수의 응답을 받음
        ResponseEntity<String> responseEntity = restTemplate.exchange(
                "https://kauth.kakao.com/oauth/token",  //요청 주소
                HttpMethod.POST,    //요청방법
                kakaoTokenRequest,  //넘기는 데이터
                String.class    //받아올 데이터 타입
        );
{	//Body부분
	"access_token": "Usae7ljOPD18PLbbO48xgbtK_AT0WZolJPlbeRxSCj11GQAAAYnQxcvs",
	"token_type": "bearer",
	"refresh_token": "475Hw-ABEQHmBF0QkHlUgncFf994FLtfMi8BwfS8Cj11GQAAAYnQxcvr",
	"expires_in": 21599,
	"scope": "account_email profile_image profile_nickname",
	"refresh_token_expires_in": 5183999
},
[
Date: "Mon, 07 Aug 2023 16:12:51 GMT",
Content - Type: "application/json;charset=utf-8",
Transfer - Encoding: "chunked",
Connection: "keep-alive",
Cache - Control: "no-cache, no-store,
max-age=0, must-revalidate",
Pragma: "no-cache",
Expires: "0",
X - XSS - Protection: "1; mode=block",
X - Frame - Options: "DENY", X - Content - Type - Options: "nosniff",
Kakao: "Talk", Access - Control - Allow - Origin: "*",
Access - Control - Allow - Methods: "GET, POST,
OPTIONS",
Access - Control - Allow - Headers: "Authorization, KA, Origin, X-Requested-With,
Content-Type, Accept"]

응답 받은 데이터는 이렇게 된다.
각각의 데이터의 설명은 카카오Developers에 참고
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token-response-body

//응답받은 responseEntity 를 살펴보면 Body에는 JSON 형태로 데이터가 들어있는데 이 JSON 형태의 데이터를 자바 오브젝트 형태로 변환해서 사용하기위해서 변환을 해준다.
//ObjectMapper 클래스를 사용해서 변환

    ObjectMapper objectMapper = new ObjectMapper();
    OAuthToken oAuthToken = objectMapper.readValue(responseEntity.getBody(), OAuthToken.class);
    

올바르게 자바 Object로 매핑된 후에 이 토큰을 갖고 사용자 정보를 갖고와보자

사용자 정보 조회하기

Developers 에서 사용자 정보를 조회하는 방법에는 Get 방식과 Post 방식이있다.

이번에도 REST API 사용법을 사용하기 때문에 이전에 토큰을 가져올때 했던 방법을 이용하자.
RestTemplate를 만들어주고 헤더를 만들어주고 바디부분을 필요없기때문에 헤더만 만들어주고 HttpEntity를 만들어줘서 헤더를 넣어주고 ResponseEntity로 응답 데이터를 받아온다.

//사용자 정보를 받아오기위한 또다른 RestTemplate를 사용해서 응답받기 (POST)
        RestTemplate restTemplate2 = new RestTemplate();
        //HttpHeader 오브젝트 생성
        HttpHeaders httpHeaders2 = new HttpHeaders();
        //Content-type 을 HttpHeader에 담는다는 것은 내가 담을 데이터가 key-value 데이터라고 알려주는 것이다
        httpHeaders2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        httpHeaders2.add("Authorization","Bearer "+ oAuthToken.getAccess_token());

        //httpBody 생성 부분
        HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest =
                new HttpEntity<>(httpHeaders2);
        //Http 요청하기 - POST방식 그리고 response 변수의 응답을 받음
        ResponseEntity<String> responseEntity2 = restTemplate.exchange(
                "https://kapi.kakao.com/v2/user/me",  //요청 주소
                HttpMethod.POST,    //요청방법
                kakaoProfileRequest,  //넘기는 데이터
                String.class    //받아올 데이터 타입
        );

받아온 정보의 Body부분을 확인하면 카카오 계정으로 로그인한 사용자의 정보가 나오는 것을 확인할 수 있다.

받아온 사용자 정보를 자바 Object로 매핑

받아온 데이터를 자바 Object로 매핑하기위해서 jsonschema2pojo 라는 웹사이트를 이용했다.
이 웹사이트를 이용하면 json의 데이터를 기반으로 각각의 속성값에 따른 클래스를 만들어줍니다.

-----------------------------------com.example.board_project.entity.KakaoAccount.java-----------------------------------

package com.example.board_project.entity;

import javax.annotation.Generated;

@Generated("jsonschema2pojo")
public class KakaoAccount {

public Boolean profileNicknameNeedsAgreement;
public Boolean profileImageNeedsAgreement;
public Profile profile;
public Boolean hasEmail;
public Boolean emailNeedsAgreement;
public Boolean isEmailValid;
public Boolean isEmailVerified;
public String email;

}
-----------------------------------com.example.board_project.entity.KakaoProfile.java-----------------------------------

package com.example.board_project.entity;

import javax.annotation.Generated;

@Generated("jsonschema2pojo")
public class KakaoProfile {

public Long id;
public String connectedAt;
public Properties properties;
public KakaoAccount kakaoAccount;

}
-----------------------------------com.example.board_project.entity.Profile.java-----------------------------------

package com.example.board_project.entity;

import javax.annotation.Generated;

@Generated("jsonschema2pojo")
public class Profile {

public String nickname;
public String thumbnailImageUrl;
public String profileImageUrl;
public Boolean isDefaultImage;

}
-----------------------------------com.example.board_project.entity.Properties.java-----------------------------------

package com.example.board_project.entity;

import javax.annotation.Generated;

@Generated("jsonschema2pojo")
public class Properties {

public String nickname;
public String profileImage;
public String thumbnailImage;

}

이렇게 만들어주게되고 수정하면

package com.example.board_project.entity;

public class KakaoProfile {

    public Long id;
    public String connectedAt;
    public Properties properties;
    public KakaoAccount kakaoAccount;

    class Properties {

        public String nickname;
        public String profileImage;
        public String thumbnailImage;

    }

    class KakaoAccount {

        public Boolean profileNicknameNeedsAgreement;
        public Boolean profileImageNeedsAgreement;
        public Profile profile;
        public Boolean hasEmail;
        public Boolean emailNeedsAgreement;
        public Boolean isEmailValid;
        public Boolean isEmailVerified;
        public String email;


        class Profile {

            public String nickname;
            public String thumbnailImageUrl;
            public String profileImageUrl;
            public Boolean isDefaultImage;

        }
    }
}

이렇게 구성하게 됩니다.
하지만 이렇게 클래스를 만들고

    ObjectMapper objectMapper2 = new ObjectMapper();
        KakaoProfile    kakaoProfile = objectMapper2.readValue(responseEntity2.getBody(), KakaoProfile.class);

ObjectMapper를 사용해서 매핑을 해주게되면 오류가 발생하는데 Inner Class가 static(정적)으로 선언되지 않는 한 단독(Outer 클래스를 참조하지 않고)으로 Inner Class의 디폴트 생성자를 호출해 인스턴스를 생성할 수 없기 때문입니다. 따라서 내부 클래스를 static으로 설정해줍니다.
자세한 내용은 오류모음 게시글에서 다루었습니다.


package com.example.board_project.entity;

import lombok.Data;

@Data
public class KakaoProfile {

    public Long id;
    public String connected_at;
    public Properties properties;
    public Kakao_account kakao_account;

    @Data
    public static class Properties {

        public String nickname;
        public String profile_image;
        public String thumbnail_image;

    }

    @Data
    public static class Kakao_account {

        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 static class Profile {

        public String nickname;
        public String thumbnail_image_url;
        public String profile_image_url;
        public Boolean is_default_image;

    }
}

이렇게 매핑까지 완료하면 kakaoProfile 변수를 출력했을때에 알맞은 정보가 나오게됩니다.
이렇게 만들어진 Java Object인 kakaoProfile을 이제 회원으로 저장하는 과정을 거쳐하는데 회원가입은 UserJoinDto에 원하는 정보를 넣어주면되는데 아이디나 비밀번호 등등 내가 만들어놓은 User객체의 속성과 맞지 않는 부분이 있기때문에 내가 카카오로 로그인한 사람의 정보에 대한 규칙을 만들어서 저장 시켜주면된다.
예를 들어서 나는,

System.out.println("블로그 서버 loginId : "+kakaoProfile.getKakao_account().getEmail()+"_"+kakaoProfile.getId());
        //사용하지 않는 쓰레기 값 넣어주기
        UUID garbagePW = UUID.randomUUID();
        System.out.println("블로그 서버 password : "+garbagePW);
        System.out.println("블로그 서버 name : " +kakaoProfile.getProperties().getNickname());
        System.out.println("블로그 서버 phoneNumber : 000-0000-0000");
        System.out.println("블로그 서버 username : "+kakaoProfile.getProperties().getNickname());
        System.out.println("블로그 서버 role : USER");

        UserJoinDto userJoinDto = UserJoinDto.builder()
                .loginId(kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId())
                .password(garbagePW.toString())
####                 .phoneNumber("000-0000-0000")
                .username(kakaoProfile.getProperties().getNickname())
                .build();

        userService.join(userJoinDto);

로그인아이디와 패스워드 기타 정보에 대한 규칙을 만들었고 이를 따라서 카카오 로그인으로 로그인을 하게되면 회원가입까지 진행하게되고 회 기존의 방법으로 회원가입한 사람을 구별할 수 있다.
또한 이전에 카카오로그인으로 회원가입을 진행한 사용자 일수도 있기 때문에 확인해준다.

        if (userService.findByLoginId(kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId()) == null) {
            userService.join(userJoinDto);
        }

회원가입 과정이 지나면 로그인 처리를 해줘야하는데 이전에 SpringSecurity로 로그인 처리를 했을때에 Bean으로 등록했던 authenticationManager를 주입받고 UsernamePasswordAuthenticationToken() 함수를 사용해서 로그인처리를 해준다.

로그인 과정에서 오류 발생

이전에 카카오 로그인으로 로그인한사람을 회원가입 시켜줄때 UUID 클래스를 사용해서 랜덤값을 넣어주었는데 이를 갖고 UsernamePasswordAuthenticationToken() 함수에 넣어주게되면 회원가입은 가능하지만 UUID값이 일정하지 않고 계속 변경되기때문에 로그인 하는 과정에서 오류가 발생하게됩니다. 따라서 고정되어있는 변수를 지정해줍니다. 이 변수는 다른사용자들이 절대 알 수 없게 비공개되어야합니다.

application.yml 수정

cos:
  key: cos1234
  jackson:
    serialization:
      fail-on-empty-beans: false

cos 값 가져오기

    @Value("${cos.key}")
    private String cosKey;

로그인 처리

Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userJoinDto.getLoginId(), cosKey));
        SecurityContextHolder.getContext().setAuthentication(authentication);

카카오 로그인 API 적용 완료

profile
멋있는 사람 - 일단 하자

0개의 댓글