[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에 저장하고 맞다면 로그인 처리를 해주면 된다.
기존의 회원가입, 로그인 코드와 동일하고 소셜 로그인이 목적이므로 이 이후의 코드는 생략.
** 다음 글은 라이브러리를 이용하여 카카오 뿐만 아니라 구글, 네이버의 로그인도 함께 사용할 수 있는 재사용 가능한 코드로 구현해볼 것이다.
♣ 참고 및 인용