카카오 로그인 REST API 문서
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
먼저 카카오톡 로그인 버튼을 다운받자
https://developers.kakao.com/tool/resource/login
다운받은 파일을 내 프로젝트의 src/main/resources/static/image에 추가하자
loginForm.jsp에 카카오 로그인 버튼을 추가하자.
url은 KakaoDeveloper 공식문서에 있는 URL을 넣어주면된다.
로그인 요청을 위한 인증 코드를 받자
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code
로그인 요청 주소 (GET 방식)
https://kauth.kakao.com/oauth/authorize?client_id=클라이언트키&redirect_uri=콜백주소&response_type=code
loginForm.jsp
자신의 클라이언트키와 콜백주소를 넣어준다.
<button id="btn-login" class="btn btn-primary">로그인</button>
<a href="https://kauth.kakao.com/oauth/authorize?client_id=클라이언트키&redirect_uri=콜백주소&response_type=code"><img height="38px" src="/img/kakao_btn.png" /></a>
동의하고 계속하기를 누르면 에러페이지와 함께 쿼리스트링으로 인증코드를 받게 된다.
http://localhost:8001/auth/kakao/callback?code=~~~~~~~~
이제 이 요청주소에 대한 컨트롤러를 만들자
UserController.java
@GetMapping("auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음
return "카카오 인증 완료" + code;
}
카카오 인증 성공
이제 인증토큰을 받아보자
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token
https://kauth.kakao.com/oauth/token 에 POST 방식으로 application/x-www/form-urlencoded:charset=utf-8 (key=value 형태)의 데이터를 던지라는 뜻
토큰 발급 주소 (POST) - http body에 데이터를 전달 (4가지 데이터)
https://kauth.kakao.com/oauth/token
UserController.java
@GetMapping("auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음
// POST 방식으로 key=value 데이터를 요청 (카카오톡으로)
/* Retrofit2
* OkHttp
* HttpsURLConnection
* RestTemplate (우린 이걸로 할거다)
*/
RestTemplate rt = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가 key=velue 형임을 명시
// HttpBody 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "내 RestAPI앱키");
params.add("redirect_uri", "http://localhost:8001/auth/kakao/callback");
params.add("code", code);
// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers); // header 와 body 값을 가지고 있는 entity 값이 된다.
// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class // String 타입으로 응답 데이터를 받겠다.
);
return "카카오 토큰 요청 완료" + response;
}
카카오 토큰 요청 성공
응답데이터인 Response에 있는 데이터를 담을 수 있는 자바 객체로 만들자
OAuthToken.java
package com.lwj.blog.model;
import lombok.Data;
@Data // parsing 할때 getter setter 필요
public class OAuthToken {
// 스펠링이 틀리지 않도록 조심하자
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private int refresh_token_expires_in;
}
UserController.java
// Gson, Json, Simple, ObjectMapper라이브러리를 사용하여 자바객체로 만들 수 있다.
ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oauthToken = null;
try {
oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 액세스 토큰 : " + oauthToken.getAccess_token());
이제 토큰을 가지고 유저 정보를 불러오자
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
UserController.java
RestTemplate rt2 = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oauthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
// key=velue 형임을 명시
// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers2);
// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
ResponseEntity<String> response2 = rt2.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST,
kakaoProfileRequest, String.class // String 타입으로 응답 데이터를 받겠다.
);
System.out.println("유저정보 : " + response2.getBody());
다시 카카오 로그인을 실행하면 데이터를 확인 할 수 있다.
이 데이터를 담을 자바 오브젝트로 만들자.
KakaoProfile.java
package com.lwj.blog.model;
import lombok.Data;
@Data
public class KakaoProfile {
public Long id;
public String connected_at;
public Properties properties;
public Kakao_account kakao_account;
@Data
public 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 class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url;
public Boolean is_default_image;
}
}
@Data
public class Properties {
public String nickname;
public String profile_image;
public String thumbnail_image;
}
}
UserController.java
// Gson, Json, Simple, ObjectMapper
ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// User 오브젝트 : userName, password, email
System.out.println("카카오 아이디(번호) : " + kakaoProfile.getId());
System.out.println("카카오 이메일 : " + kakaoProfile.getKakao_account().getEmail());
이제 마지막으로 카카오계정으로 로그인 및 회원가입을 구현해보자
User.java
length를 30 -> 100 변경
@Column(nullable=false, unique = true, length = 100)
private String userName; // 아이디
UserController.java
// 상단에 추가
@Value("${lwj.key}")
private String lwjKey;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
--------------------------------------------------------------------------
System.out.println("블로그서버 유저네임 : " + kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId());
System.out.println("블로그서버 이메일 : " + kakaoProfile.getKakao_account().getEmail());
// UUID garbagePassword = UUID.randomUUID();
// UUID란 -> 중복되지 않는 어떤 특정 값을 만들어내는 알고리즘
// System.out.println("블로그서버 패스워드 : " + garbagePassword); // DB에 넣기위한 랜덤 임시 비밀번호
System.out.println("블로그서버 패스워드 : " + lwjKey); // DB에 넣기위한 랜덤 임시 비밀번호
User kakakoUser = User.builder()
.userName( kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId())
.password(lwjKey)
.email(kakaoProfile.getKakao_account().getEmail())
.build();
// 이미 등록된 회원이면 로그인 , 등록되있지 않은 회원일 경우 로그인을 하는 분기 처리를 해야한다.
// 가입자 혹은 비가입자 체크 해서 처리
User originUser = userService.회원찾기(kakakoUser.getUserName());
if(originUser.getUserName() == null) {
System.out.println("기존 회원이 아닙니다. ~~~");
userService.회원가입(kakakoUser);
}
System.out.println("자동 로그인을 진행합니다.");
// 로그인 처리
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(kakakoUser.getUserName(), lwjKey));
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/";
application.yml
lwj:
key: lwj1234 # 카카오 회원가입한 회원들의 비밀번호, 절대로 노출되면 안된다.
하지만 OAuth로 로그인 한 회원은 회원정보를 수정하지 못하게 막아야한다.
User.java
먼저 User객체에 이 회원이 OAuth로 회원가입을 했는지 일반회원가입을 했는지 담아주는 속성을 추가해준다.
private String oauth; // kakao, google
updateForm.jsp
<!-- 이렇게만 막으면 보안에 취약하다 서버쪽에서도 막아줘야한다. -->
<c:if test="${empty principal.user.oauth }">
<div class="form-group">
<label for="pwd">Password</label>
<input type="password" class="form-control" placeholder="password" id="password">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" >
</div>
</c:if>
<div class="form-group">
<label for="email">Email</label>
<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" readonly>
</div>
UserService.java
@Transactional
public User 회원찾기(String userName) {
User user = userRepository.findByUserName(userName).orElseGet(()->{
return new User(); // 비어있는 객체 반환
});
return user;
}
@Transactional
public void 회원수정(User user) {
// 수정시에는 영속성 컨텍스트 User 오브젝트를 영속화시키고, 영속화된 User 오브젝트를 수정
// select 를 해서 User 오브젝트를 DB로 부터 가져오는 이유는 영속화를 하기 위해서 !!
// 영속화된 오브젝트를 변경하면 자동으로 DB에 update 문을 날려준다
User persistance = userRepository.findById(user.getId()).orElseThrow(()->{
return new IllegalArgumentException("회원 찾기 실패");
});
// Validate 체크 -> oauth 필드에 값이 없으면 수정가능
if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword);
persistance.setPassword(encPassword);
persistance.setEmail(user.getEmail());
}
// 회원수정 함수 종료시 = 서비스 종료시 = 트랜잭션 종료 = commit이 자동으로 된다.
// 영속화된 persistance 객체의 변화가 감지되면 더티체킹이 되어 update문을 날려줌.
}
전체코드
UserController.java
@Controller
public class UserController {
@Value("${lwj.key}")
private String lwjKey;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@GetMapping("auth/kakao/callback")
public String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음
// POST 방식으로 key=value 데이터를 요청 (카카오톡으로)
/*
* Retrofit2 OkHttp HttpsURLConnection RestTemplate (우린 이걸로 할거다)
*/
RestTemplate rt = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
// key=velue 형임을 명시
// HttpBody 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "b9d1d15c7c76c5b18e989b19383acaf0");
params.add("redirect_uri", "http://localhost:8001/auth/kakao/callback");
params.add("code", code);
// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers); // header 와 body 값을 가지고 있는 entity 값이 된다.
// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class // String 타입으로 응답 데이터를 받겠다.
);
// Gson, Json, Simple, ObjectMapper라이브러리를 사용하여 자바객체로 만들 수 있다.
ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oauthToken = null;
try {
oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 액세스 토큰 : " + oauthToken.getAccess_token());
RestTemplate rt2 = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oauthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
// key=velue 형임을 명시
// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers2);
// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
ResponseEntity<String> response2 = rt2.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST,
kakaoProfileRequest, String.class // String 타입으로 응답 데이터를 받겠다.
);
System.out.println("유저정보 : " + response2.getBody());
// Gson, Json, Simple, ObjectMapper
ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// User 오브젝트 : userName, password, email
System.out.println("카카오 아이디(번호) : " + kakaoProfile.getId());
System.out.println("카카오 이메일 : " + kakaoProfile.getKakao_account().getEmail());
System.out.println("블로그서버 유저네임 : " + kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId());
System.out.println("블로그서버 이메일 : " + kakaoProfile.getKakao_account().getEmail());
// UUID garbagePassword = UUID.randomUUID();
// UUID란 -> 중복되지 않는 어떤 특정 값을 만들어내는 알고리즘
// System.out.println("블로그서버 패스워드 : " + garbagePassword); // DB에 넣기위한 랜덤 임시 비밀번호
System.out.println("블로그서버 패스워드 : " + lwjKey); // DB에 넣기위한 랜덤 임시 비밀번호
User kakakoUser = User.builder()
.userName( kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId())
.password(lwjKey)
.email(kakaoProfile.getKakao_account().getEmail())
.oauth("kakao")
.build();
// 이미 등록된 회원이면 로그인 , 등록되있지 않은 회원일 경우 로그인을 하는 분기 처리를 해야한다.
// 가입자 혹은 비가입자 체크 해서 처리
User originUser = userService.회원찾기(kakakoUser.getUserName());
if(originUser.getUserName() == null) {
System.out.println("기존 회원이 아닙니다. ~~~");
userService.회원가입(kakakoUser);
}
System.out.println("자동 로그인을 진행합니다.");
// 로그인 처리
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(kakakoUser.getUserName(), lwjKey));
SecurityContextHolder.getContext().setAuthentication(authentication);
// OAuth로 로그인한 회원은 회원정보를 수정하지 못하게 해야된다.
return "redirect:/";
}
}
UserService.java
@Service
// service : 트랜젝션 관리를 위해 사용
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder encoder;
@Transactional
public User 회원찾기(String userName) {
User user = userRepository.findByUserName(userName).orElseGet(()->{
return new User(); // 비어있는 객체 반환
});
return user;
}
@Transactional
public void 회원수정(User user) {
// 수정시에는 영속성 컨텍스트 User 오브젝트를 영속화시키고, 영속화된 User 오브젝트를 수정
// select 를 해서 User 오브젝트를 DB로 부터 가져오는 이유는 영속화를 하기 위해서 !!
// 영속화된 오브젝트를 변경하면 자동으로 DB에 update 문을 날려준다
User persistance = userRepository.findById(user.getId()).orElseThrow(()->{
return new IllegalArgumentException("회원 찾기 실패");
});
// Validate 체크 -> oauth 필드에 값이 없으면 수정가능
if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword);
persistance.setPassword(encPassword);
persistance.setEmail(user.getEmail());
}
// 회원수정 함수 종료시 = 서비스 종료시 = 트랜잭션 종료 = commit이 자동으로 된다.
// 영속화된 persistance 객체의 변화가 감지되면 더티체킹이 되어 update문을 날려줌.
}
}
User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable=false, unique = true, length = 100)
private String userName; // 아이디
@Column(nullable=false, length = 100)
// 100은 너무 길잖아 why ? 123456 => 해쉬 (비밀번호 암호화-> 엄청길다)
private String password; // 비밀번호
@Column(nullable=false, length = 50)
private String email; // 이메일
@Enumerated(EnumType.STRING)
private RoleType role; // ADMIN, USER 만 값이 들어가게된다.
private String oauth; // kakao, google
@CreationTimestamp // 현재시간으로 시간이 자동 입력
private Timestamp createDate;
}
KakaoProfile.java
@Data
public class KakaoProfile {
public Long id;
public String connected_at;
public Properties properties;
public Kakao_account kakao_account;
@Data
public 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 class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url;
public Boolean is_default_image;
}
}
@Data
public class Properties {
public String nickname;
public String profile_image;
public String thumbnail_image;
}
}
OAuthToken.java
@Data // parsing 할때 getter setter 필요
public class OAuthToken {
// 스펠링이 틀리지 않도록 조심하자
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private int refresh_token_expires_in;
}
updateForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ include file="../layout/header.jsp"%>
<div class="container">
<form>
<input type="hidden" id="id" value="${principal.user.id }">
<div class="form-group">
<label for="userName">UserName</label>
<input type="text" value="${ principal.user.userName }" class="form-control" placeholder="userName" id="userName" readonly>
</div>
<!-- 이렇게만 막으면 보안에 취약하다 서버쪽에서도 막아줘야한다. -->
<c:if test="${empty principal.user.oauth }">
<div class="form-group">
<label for="pwd">Password</label>
<input type="password" class="form-control" placeholder="password" id="password">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" >
</div>
</c:if>
<div class="form-group">
<label for="email">Email</label>
<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" readonly>
</div>
</form>
<button id = "btn-update" class="btn btn-primary">회원수정완료</button>
</div>
<script src="/js/user.js"></script>
<%@ include file="../layout/footer.jsp"%>