💡 소셜 로그인을 구현하기 위해서는 OAuth를 통해 사용자를 인증하고 사용자의 정보를 가져올 필요가 있다.
OAuth 작동 방식은 위의 그림과 같이 간단히 도식화할 수 있고 이를 본 프로젝트에 적용해보았다.
...
const kakao_url = `https://kauth.kakao.com/oauth/authorize?response_type=code
&client_id=${apikey.rest_api_key}
&redirect_uri=${redirect_urls.kakaoLogin_redirect_url}`;
// 카카오 인가코드를 받아옴 ( REST_API_KEY , REDIRECT_URL 필요 )
function kakao_getCode(){
// 현재 페이지를 대체
window.location.replace(kakao_url);
}
// Redirect URL 예시
// http://localhost:3000/login/redirect/kakao?code=089pzhwqPtNa21ZVJDi1X--gZDCjUSl8jP-rlEGFd7eO3UvdhzzfiemV-jqrmVaLg_X3EAo9c04AAAGDp4NwPQ
const kakao_code = window.location.search.split("code=")[1] ;
axios({
method: "post",
url: "https://kauth.kakao.com/oauth/token",
params: {
"grant_type" : "authorization_code" ,
"client_id" : rest_api_key ,
"redirect_url" : redirect_url ,
"code" : kakao_code ,
}
})
.then( async(res) => {
// 서버에 요청 ( Promise 함수 호출을 통해 jwt 리턴 보장 )
api.registerUserWithKakao( res.data.access_token )
.then(async (res) => {
// SessionStorage에 jwt 저장
window.sessionStorage.setItem("Auth", res.accessToken);
window.sessionStorage.setItem("Refresh", res.refreshToken);
api.updateFcm(window.sessionStorage.getItem("fcm"))
.then(response =>{
if( res.isAlreadyRegister ){
// 미니홈피페이지로 이동
navigate("/", {replace: true});
}
else{
navigate("/makeusername", {replace: true});
}
}).catch(e=>{
console.log(e)
})
});
})
.catch( e => {
// 로그인 또는 회원가입 실패
navigate("/login");
});
body : {
...
data : { ...,"access_token" : ${액세스_토큰값} },
...
}
// 서버에 요청 ( Promise 함수 호출을 통해 jwt 리턴 보장 )
api.registerUserWithKakao( res.data.access_token )
.then(async (res) => {
// SessionStorage에 jwt 저장
window.sessionStorage.setItem("Auth", res.accessToken);
window.sessionStorage.setItem("Refresh", res.refreshToken);
api.updateFcm(window.sessionStorage.getItem("fcm"))
.then(response =>{
if( res.isAlreadyRegister ){
// 미니홈피페이지로 이동
navigate("/", {replace: true});
}
else{
navigate("/makeusername", {replace: true});
}
}).catch(e=>{
console.log(e)
})
});
function registerUserWithKakao(token){
return new Promise((resolve,reject) => {
request({
method: 'POST' ,
url: '/api/user/register/kakao',
headers: {
token: token
// "Access-Control-Allow-Origin" : true
},
})
.then( res => {
// Jwt 반환
if ( res.data.statusCode === status.POST_SUCCESS ){
resolve (res.data.data);
}
})
.catch( e => {
console.log(e);
reject();
})
});
}
// 카카오 로그인 및 회원가입 요청
@PostMapping("/api/user/register/kakao")
public ResponseEntity<ApiResponse> registerUserWithKakao(HttpServletRequest request,HttpServletResponse response) throws ParseException {
response.addHeader("Access-Control-Expose-Headers", "Auth");
response.addHeader("Access-Control-Expose-Headers", "Refresh");
// 카카오로 부터 받아온 정보로 유저로 등록
Map<String, Object> stringObjectMap = userService.addKakaoUser(request.getHeader("token"));
String jwt = (String) stringObjectMap.get("accessToken");
String refresh = (String) stringObjectMap.get("refreshToken");
boolean isAlreadyRegister = (boolean) stringObjectMap.get("isAlreadyRegister");
HttpHeaders headers = new HttpHeaders();
headers.add("Auth", jwt);
headers.add("Refresh",refresh);
return new ResponseEntity<>(ApiResponse.response(
HttpStatusCode.POST_SUCCESS,
HttpResponseMsg.POST_SUCCESS,
stringObjectMap), headers, HttpStatus.OK);
}
다음은 서비스단계의 코드다.
이전 컨트롤러에서 addKakaoUser 메소드를 살펴보자.
@Transactional
public Map<String,Object> addKakaoUser(String accessToken){
//받은 String정보를 JSON 객체화
JSONObject kakaoUserInfoJson = getKakaoUserInfoByAccessToken(accessToken);
JSONObject userInfoJson = new JSONObject((LinkedHashMap) kakaoUserInfoJson.get("kakao_account"));
JSONObject profileJson = new JSONObject((LinkedHashMap) userInfoJson.get("profile"));
// 필요한 정보들
String email = userInfoJson.getAsString("email");
String profileUrl = profileJson.getAsString("profile_image_url");
return addUser(email,profileUrl);
}
public JSONObject getKakaoUserInfoByAccessToken(String token)
{
String apiURL = "https://kapi.kakao.com/v2/user/me";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization","Bearer "+ token);
HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<JSONObject> userInfoResponse = rt.exchange(
apiURL,
HttpMethod.GET,
entity,
JSONObject.class
);
return userInfoResponse.getBody();
}
private Map<String,Object> addUser(String email,String profileUrl){
// 유저가 이미 DB에 존재하는지 확인
Optional<User> findUser = userRepository.findByEmail(email);
Map<String,Object> map = new HashMap<>();
// 존재 유무 isPresent()로 확인
if( findUser.isPresent() ){
map.put("accessToken", jwtTokenProvider.createToken(findUser.get().getEmail(),findUser.get().getRole()));
map.put("refreshToken",jwtTokenProvider.createRefreshToken(findUser.get().getEmail(),findUser.get().getRole()));
map.put("isAlreadyRegister", true);
return map;
} else{
User newUser = User.builder()
.email(email)
.profileUrl(profileUrl)
.repo_url(" ")
.role(Role.USER)
.nickName(email.split("/")[0])
.build();
User savedUser = userRepository.save(newUser);
map.put("accessToken", jwtTokenProvider.createToken(savedUser.getEmail(), Role.USER));
map.put("refreshToken",jwtTokenProvider.createRefreshToken(savedUser.getEmail(),Role.USER));
map.put("isAlreadyRegister", false);
return map;
}
OAuth를 통해 가져온 유저의 이메일로 본 서버의 DB에 조회하면 두가지 경우가 생긴다.
(1) 회원가입인 경우 ( 특정 유저가 첫 로그인을 한 경우 )
(2) 이미 가입된 경우 ( DB에 유저가 있는 경우 )
(공통)
컨트롤러로 돌아가면 ?
// 카카오로 부터 받아온 정보로 유저로 등록
Map<String, Object> stringObjectMap = userService.addKakaoUser(request.getHeader("token"));
String jwt = (String) stringObjectMap.get("accessToken");
String refresh = (String) stringObjectMap.get("refreshToken");
boolean isAlreadyRegister = (boolean) stringObjectMap.get("isAlreadyRegister");
HttpHeaders headers = new HttpHeaders();
headers.add("Auth", jwt);
headers.add("Refresh",refresh);
return new ResponseEntity<>(ApiResponse.response(
HttpStatusCode.POST_SUCCESS,
HttpResponseMsg.POST_SUCCESS,
stringObjectMap), headers, HttpStatus.OK);
api.registerUserWithKakao( res.data.access_token )
.then(async (res) => {
// SessionStorage에 jwt 저장
window.sessionStorage.setItem("Auth", res.accessToken);
window.sessionStorage.setItem("Refresh", res.refreshToken);
api.updateFcm(window.sessionStorage.getItem("fcm"))
.then(response =>{
if( res.isAlreadyRegister ){
// 미니홈피페이지로 이동
navigate("/", {replace: true});
}
else{
navigate("/makeusername", {replace: true});
}
}).catch(e=>{
console.log(e)
})
});
위의 클라이언트 단 코드를 일부 가져와 다시 살펴보면,