본문 바로가기

카테고리 없음

[OAuth2.0] 소셜 로그인 구현하기 -3- 카카오 로그인

 

[OAuth2.0] 소셜 로그인 구현하기 -1- OAuth란?

[OAuth2.0] 소셜 로그인 구현하기 -2- 카카오 어플리케이션 등록

[OAuth2.0] 소셜 로그인 구현하기 -3- 카카오 로그인

[OAuth2.0] 소셜 로그인 구현하기 -4- 카카오, 네이버, 구글 로그인

 

 


 

** 앞에서 어플리케이션 등록으로 client_id, client_secret, redirect_uri를 준비해두었다. 

이제 스프링에서 직접 구현해보자.

 인증과 로그인 과정을 크게 3가지로 나누었다.

 

1. 인증 코드 요청&전달 받기

2. 토큰 요청&전달 받기

3. 유저 정보 요청&전달 받기

  ....

4. 그 이후  

 


    1. 인증 코드 요청 & 전달 받기   

 

@Controller
@RequestMapping("/login/")
public class KakaoTestController {

	@Autowired
	KakaoDAO kakao;

	//카카오 로그인 페이지로
	@GetMapping("kakao")
	public RedirectView kakaoLogin() {
		RedirectView redirectView = new RedirectView();
		String reqUri = kakao.getCodeUri();
		redirectView.setUri(reqUri);
		return redirectView;
	}
}

 

  • view에서 카카오로 로그인 버튼 클릭시 이곳으로 매핑된다. 
  • 컨트롤러에서는 KakaoDAO의 getCodeUri 메서드로부터 카카오 로그인에 필요한 URI을 조립한 주소를 받고 리다이렉트 시킨다. 

 

  • 요청할 URI의 호스트와 전달할 파라미터들은 다음과 같다.
GET /oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com

 

파라미터 타입 설명 필수여부
client_id String 앱 생성 시 발급받은 REST API 키 O
redirect_uri String 인가 코드가 리다이렉트될 URI O
response_type String code로 고정 O
state String 로그인 요청과 콜백 간에 상태를 유지하기 위해 사용되는 임의의 문자열(정해진 형식 없음)
Cross-Site Request Forgery(CSRF) 공격으로부터 보호하기 위해 해당 파라미터 사용을 권장함
X
prompt String 동의 화면 요청 시 추가 상호작용을 요청하고자 할 때 전달하는 파라미터
쉼표(,)로 구분된 문자열 값 목록으로 전달
X
  • 간략하게 필수가 아닌 파라미터들은 생략하고 필수 파라미터들만 전달해보자.
@Service
public class KakaoDAO {
	
	@Autowired
	private SqlSessionTemplate sqlSession;
	
	final private String client_id = "~~~~~";
	final private String client_secret = "~~~~~";
	final private String redirect_uri= "~~~~~";
    
	 //카카오 로그인 URL 건네주기 
	 public String getCodeUri() { 
		 String reqUri
		 	= "https://kauth.kakao.com/oauth/authorize?client_id="
		 			+client_id+"&redirect_uri="+redirect_uri+"&response_type=code"; 
		 return	reqUri; 
	 }
}

 

  • DAO에서 계속 쓸 client_id, client_secret, redirect_uri를 final로 수정불가 멤버변수로 지정해둔다.
  • 나중에는 properties로 따로 빼서 관리할 것이다. 
  • 이 메서드에서 URI를 조립하고 컨트롤러로 전달하고, 컨트롤러에서는 이 주소를 리다이렉트 시킨다.

 

  • 리다이렉트된 카카오 로그인 화면이다. 저기에 사용자의 카카오 아이디와 비밀번호를 제대로 입력시 

 

  • 사용자가 내 어플리케이션을 이용하는 데 있어 제공하는 항목에 대한 동의를 받는 창이 뜬다. 
  • 동의 항목은 이전 글의 내 어플리케이션 만들기에서 설정해두었던 것이다. 
  • 여기선 이메일과 닉네임을 필수로, 성별을 선택사항으로 하였다. 
HTTP/1.1 302 Found
Content-Length: 0
Location: {REDIRECT_URI}?code={AUTHORIZE_CODE}
  • 사용자가 동의하기를 누르면 카카오 서버에서는 요청했던 code를 미리 설정해두었던 redirect_uri에 Get방식으로 전달한다.

 


    2. 토큰 요청 & 전달 받기    

 

@Controller
@RequestMapping("/login/")
public class KakaoTestController {

	@Autowired
	KakaoDAO kakao;
	
	@Autowired
	UserDAO uao;
	
 	 @GetMapping("kakao/LoginAndJoin/")
	 public ModelAndView kakaoLoginAndJoin(@RequestParam("code") String code, HttpSession session, HttpServletRequest request) {
      	ModelAndView mv = new  ModelAndView();

     	 //액세스 토큰 받기
     	 String access_Token = kakao.getAccessToken(code);
	
		... 유저 정보 요청 ...
		... 가입 여부 확인 ...
		... 에 따라 로그인 ...
		... 또는 가입 처리 ...

      return mv;
  }
}

 

  • redirect_uri로 설정해둔 컨트롤러의 메서드이다. 이쪽으로 code가 전달될 것이다. 
  • 카카오로부터 유저정보를 얻기 위해선 로그인 한 유저의 액세스 토큰이 필요하다.
  • 액세스 토큰을 얻기 위해 code를 kakaoDAO의 getAccessToken 메서드 호출의 매개변수로 전달한다. 

 

POST /oauth/token HTTP/1.1
Host: kauth.kakao.com
Content-type: application/x-www-form-urlencoded;charset=utf-8
파라미터 타입 설명 필수여부
grant_type String authorization_code로 고정 O
client_id String 앱 생성 시 발급받은 REST API O
redirect_uri String 인가 코드가 리다이렉트된 URI O
code String 인가 코드 받기 요청으로 얻은 인가 코드 O
client_secret String 토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드
[내 애플리케이션] > [보안]에서 설정 가능
ON 상태인 경우 필수 설정해야 함
X
  • 요청할 카카오의 Host와 파라미터명이다.
  • 이전 글에서도 말했다시피 secret은 카카오에선 필수가 아니나 다른 소셜 서비스에서는 필수로 받으며 보안상 중요하기 때문에 사용하기로 한다.  

 

  • 아래는 카카오에서 제공하는 요청과 응답의 샘플이다.
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
 -d "grant_type=authorization_code" \
 -d "client_id={REST_API_KEY}" \
 -d "redirect_uri={REDIRECT_URI}" \
 -d "code={AUTHORIZATION_CODE}"
  • 이렇게 요청을 보내면 아래와 같이 응답이 JSON으로 넘어올 것이다.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
    "token_type":"bearer",
    "access_token":"{ACCESS_TOKEN}",
    "expires_in":43199,
    "refresh_token":"{REFRESH_TOKEN}",
    "refresh_token_expires_in":25184000,
    "scope":"account_email profile"
}

 

  • 그럼 액세스 토큰을 얻어보자. 
@Service
public class KakaoDAO {

	@Autowired
	private SqlSessionTemplate sqlSession;
	
	final private String client_id = "~~~~";
	final private String client_secret = "~~~~";
	final private String redirect_url= "~~~~~";

	//액세스 토큰 얻기
	public String getAccessToken(String authrize_code) {
		String access_Token ="";
		String refresh_Token ="";
		String reqURL="https://kauth.kakao.com/oauth/token";
		
		try {
			URL url = new URL(reqURL);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			
			// Post요청을 위해 기본값이 false인 setDoOutput을 true로
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);
			
			// Post요청에 필요로 요구하는 파라미터를 스트림을 통해 전송
			BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
			StringBuilder sb = new StringBuilder();
			sb.append("grant_type=authorization_code");
			sb.append("&client_id="+client_id);
			sb.append("&redirect_uri="+redirect_url);
			sb.append("&code="+authrize_code);
			bw.write(sb.toString());
			bw.flush();
			
			// 결과 코드가 200이라면 성공
			int responseCode = conn.getResponseCode();
			
			//요청을 통해 얻은 Json타입의 Response 메세지 읽어오기
			BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			String line ="";
			String result ="";
			
			while((line = br.readLine())!=null) {
				result += line;
			}
			
			JsonElement element = JsonParser.parseString(result);
			
			access_Token = element.getAsJsonObject().get("access_token").getAsString();
			refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();
			
			br.close();
			bw.close();
			
		} catch (Exception e) {
			e.printStackTrace();
		}

		return access_Token;
	}
}
  • 카카오 서버에 HttpUrlConnection을 이용해 request를 보낼 것이다. 
  • 이때 전달값들을 스트림을 통해 전송하고 요청에 대한 응답을 스트림을 통해 한줄 한줄 받아온다. 
  • 이 받은 결과는 String이므로 Gson라이브러리를 통해 Json으로 파싱을 하였고 
  • 파싱한 json에서 액세스 토큰과 리프레시 토큰을 추출하여 리턴해준다. 
  • 여기선 일단 유저 정보를 가져올 액세스 토큰만 리턴하여주었다. 

 

 

 


    3. 유저 정보 요청&전달 받기    

 

@Controller
@RequestMapping("/login/")
public class KakaoTestController {

	@Autowired
	KakaoDAO kakao;
	
	@Autowired
	UserDAO uao;
	
	@GetMapping("kakao/LoginAndJoin/")
	public ModelAndView kakaoLoginAndJoin(@RequestParam("code") String code, HttpSession session, HttpServletRequest request) {
		
        ModelAndView mv = new  ModelAndView();

		//액세스 토큰 받기
		String access_Token = kakao.getAccessToken(code);

		//유저 정보 요청
		Map<String, String> userInfo = kakao.getUserInfo(access_Token);

		... 가입 여부 확인 ...
		... 에 따라 로그인 ...
		... 또는 가입 처리 ...  

		return mv;
	}
    
}

 

  • 위에서 받은 액세스 토큰을 KakaoDAO의 getUserInfo 메서드의 매개변수로 넣어준다. 
  • 이 메서드에서 액세스 토큰으로 다시 카카오 서버에 요청하는데 이번에는 유저 정보를 요청할 것이다.
GET/POST /v2/user/me HTTP/1.1
Host: kapi.kakao.com
Authorization: Bearer {ACCESS_TOKEN}
Content-type: application/x-www-form-urlencoded;charset=utf-8

 

 

 

파라미터 설명 필수여부
Authorization 사용자 인증 수단, 액세스 토큰 값
Authorization: Bearer {ACCESS_TOKEN}
O
  • 이번엔 별도의 파라미터 없이 액세스 토큰만 보낸다.

 

@Service
public class KakaoDAO {

	@Autowired
	private SqlSessionTemplate sqlSession;
	
	final private String client_id = "~~~~";
	final private String client_secret = "~~~~";
	final private String redirect_url= "~~~~~";

	//유저 정보 얻기
	public HashMap<String, String> getUserInfo(String access_Token) {
		HashMap<String, String> userInfo = new HashMap<String, String>();
		String reqURL = "https://kapi.kakao.com/v2/user/me";
		
		try {
			URL url = new URL(reqURL);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestMethod("POST");
			
			//요청에 필요한 Header에 포함될 내용
			conn.setRequestProperty("Authorization", "Bearer "+access_Token);
			
			// 결과 코드가 200이 나오면 성공
			int responseCode = conn.getResponseCode();
			System.out.println("responseCode = " + responseCode );
			
			BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			
			String line="";
			String result ="";
			
			while((line = br.readLine()) != null) {
				result += line;
			}
			
			JsonElement element = JsonParser.parseString(result);

			JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
			JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();

			String kakaoId = element.getAsJsonObject().get("id").getAsString();
			String nickname = properties.getAsJsonObject().get("nickname").getAsString();
			String email = kakao_account.getAsJsonObject().get("email").getAsString();
			String gender = null;
	        
			userInfo.put("id", kakaoId);	//필수
			userInfo.put("email", email);	//필수
			userInfo.put("nickname", nickname);	//필수
			try {
				gender = kakao_account.getAsJsonObject().get("gender").getAsString();
			
			} catch (Exception e) {
				 gender = "성별";
			}
			userInfo.put("userGender", gender);	//선택

		} catch (Exception e) {
			e.printStackTrace();
		}
		return userInfo;
	}
	
}

 

  • 위에서 code로 액세스 토큰 받을 때와 같은 방법이다. 
  • 응답으로부터 필요한 id(개인 카카오 식별 번호)와 이메일, 닉네임을 추출하여 Map에 담아 리턴해준다.
  • 아래는 카카오에서 제공하는 응답 샘플이다.
HTTP/1.1 200 OK
{
    "id":123456789,
    "kakao_account": { 
        "profile_needs_agreement": false,
        "profile": {
            "nickname": "홍길동",
            "thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
            "profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
            "is_default_image":false
        },
        "email_needs_agreement":false, 
        "is_email_valid": true,   
        "is_email_verified": true,   
        "email": "sample@sample.com",
        "age_range_needs_agreement":false,
        "age_range":"20~29",
        "birthday_needs_agreement":false,
        "birthday":"1130",
        "gender_needs_agreement":false,
        "gender":"female"
    },  
    "properties":{
        "nickname":"홍길동카톡",
        "thumbnail_image":"http://xxx.kakao.co.kr/.../aaa.jpg",
        "profile_image":"http://xxx.kakao.co.kr/.../bbb.jpg",
        "custom_field1":"23",
        "custom_field2":"여"
        ...
    }
}

 

 


 

    4. 그 이후     

 

이후엔 받은 정보를 바탕으로 회원가입(DB에 저장된) 회원인지 체크 후

아니라면 회원가입 시켜 DB에 저장하고 맞다면 로그인 처리를 해주면 된다. 

기존의 회원가입, 로그인 코드와 동일하고 소셜 로그인이 목적이므로 이 이후의 코드는 생략.

 

 

 

** 다음 글은 라이브러리를 이용하여 카카오 뿐만 아니라 구글, 네이버의 로그인도 함께 사용할 수 있는 재사용 가능한 코드로 구현해볼 것이다. 

 

 

 

 

♣ 참고 및 인용