일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 자바스크립트
- 백엔드개발자
- 자바의정석
- 국비지원코딩
- React
- 리액트
- 혼공컴운
- 국비지원
- db
- 알고리즘
- 코딩
- 미라클모닝
- 자바알고리즘
- 운영체제
- 스프링부트
- 코드업
- 개발자일기
- 백엔드
- 소셜로그인구현
- 프로세스
- 프로그래머스
- SpringBoot
- 자바개발자
- 자바
- Codeup
- 데이터베이스
- 프로그래머
- 개발자
- 프로그래밍
- Java
- Today
- Total
초코딩(chocoding)
[Project - Spring Boot] 소셜 로그인/API - 카카오 : 로그인 유저 정보 db에 저장 / mysql 사용(workbench) / 복합키 지정 / 유저 중복 불가 / @RequiredArgsConstructor 사용 이유 본문
[Project - Spring Boot] 소셜 로그인/API - 카카오 : 로그인 유저 정보 db에 저장 / mysql 사용(workbench) / 복합키 지정 / 유저 중복 불가 / @RequiredArgsConstructor 사용 이유
sweetychocoding 2024. 1. 19. 00:47오늘은 저번에 불러온 고객의 정보를 db에 담는 작업을 했다.
내가 이 프로젝트를 주된 목적은 소셜 로그인을 위해 각 소셜의 api를 써보기 위함이다.
따라서 db구성도 최소한의 것만 하였고 굳이 회원가입과 로그인을 나누지 않았다.
따라서 내가 생각한 최소한의 구현 방식은
1. kakao를 통해 불러온 유저 정보 db user table에 담기
2. oauthtype과 oauthtoken을 복합키로 설정하여 oauthId값을 만듬 (user 중복여부를 구분하기 위함)
3. db의 저장된 user의 중복 여부 체크
이정도 이다.
먼저, 내가 구글링 했을 당시에 db에 user 정보를 저장하는 것과
저장한다고 해도 어떤 의도를 가지고 컬럼을 구성했는지에 대한 정보가 부족하였다.
너무 너무... 정보가 없어 힘들었기 때문에
내가 구성한 db를 기록할까 한다.
우선 앞서 밝혔다시피 나는 최소한의 것을 구성/구현했기 때문에 참고, 방향성 잡기 정도로만 보길 바란다.
.
.
.
내가 카카오에서 가져올 수 있는 정보는
프로필 사진, user 이름, user email 정도였다.
어쨌든 중복 처리를 하기 위해서 primary key를 지정해야 하는데
어떻게 지정할까 고민하다가
처음에는 auto increment를 하였다.
하지만 auto increment를 해봤자 그 값을 사용하지도 않을텐데
굳이? 라는 생각이 들었고
카카오가 기본으로 생성하는 랜덤 형태의 id (oauthtoken)와 user가 가입한 경로(oauthtype)을
복합키로 지정하였다.
또한 로그인과 회원가입을 따로 나누지 않고 진행하였기 때문에
회원 중복 체크는 더더욱 필수적이었다.
중복체크를 진행하지 않을 시에는
카카오로 로그인하기 라는 버튼을 누를 때마다
같은 user의 정보가 계속해서 담긴다.
Optional<User>로 검사하여 만약 중복된 OauthId 객체가
있을 경우, 로그인 하게 되고
없을 경우, 회원가입과 로그인이 동시에 이루어지게 된다.
아래는 위의 모든 설명을 구현한 코드이다.
.
.
.
KakaoAPI (Service)
package com.cm.personalProject.service;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.cm.personalProject.domain.OauthId;
import com.cm.personalProject.entity.User;
import com.cm.personalProject.repository.UserRepository;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Service
@RequiredArgsConstructor
@Log4j2
public class KakaoAPI {
private final UserRepository repository;
public String getAccessToken(String authorize_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=****0b218caf1b7c5c6faed3222c37d3");
sb.append("&redirect_uri=http://localhost:8080/social/login");
sb.append("&code=" + authorize_code);
sb.append("&client_secret=****Gb1FU2Xa3QcDKodnB1zTzEQLzGn5");
bw.write(sb.toString());
bw.flush();
// 결과 코드가 200이라면 성공
int responseCode = conn.getResponseCode();
System.out.println("responseCode : " + responseCode);
// 요청을 통해 얻은 JSON타입의 Response 메세지 읽어오기
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
System.out.println("response body : " + result);
// Gson 라이브러리에 포함된 클래스로 JSON파싱 객체 생성
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(result);
access_Token = element.getAsJsonObject().get("access_token").getAsString();
refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();
System.out.println("access_token : " + access_Token);
System.out.println("refresh_token : " + refresh_Token);
br.close();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
return access_Token;
} // getAccessToken()
// getUserInfo
public String getUserInfo(String access_token) {
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);
// conn.setRequestProperty("Content-type",
// "application/x-www-form-urlencoded;charset=utf-8");
int responseCode = conn.getResponseCode();
log.info("[KakaoApi.getUserInfo] responseCode : {}", responseCode);
BufferedReader br;
if (responseCode >= 200 && responseCode <= 300) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String line = "";
StringBuilder responseSb = new StringBuilder();
while ((line = br.readLine()) != null) {
responseSb.append(line);
}
String result = responseSb.toString();
log.info("responseBody = {}", result);
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(result);
JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();
String nickname = properties.getAsJsonObject().get("nickname").getAsString();
String email = kakao_account.getAsJsonObject().get("email").getAsString();
String token_id = element.getAsJsonObject().get("id").getAsString();
Optional<User> opt_user = repository.findById(new OauthId("kakao", token_id));
if (!opt_user.isPresent()) {
User user = new User();
user.setUseremail(email);
user.setUsername(nickname);
user.setOauthtype("kakao");
user.setOauthtoken(token_id);
repository.save(user);
} else {
return "fail";
}
br.close();
return "success";
} catch (Exception e) {
e.printStackTrace();
return "fail";
}
}
}
service 구현 시에, @Autowired 대신 @RequiredArgsConstructor을 사용하기를 권장하고 있다.
- 단일 책임 원칙 관점
- field에 final 키워드 사용
- 순환 참조 방지
- 결합도 낮춰 테스트 용이
하지만 처음에 내가 service에서 UserRepository repository;로 불러오니 @RequiredArgsConstructor을 사용해도 nullpointerException이 발생하였다. 이유를 찾아보니 다음과 같았다.
@RequiredArgsConstructor는 final이나 @NonNull로 표시된 필드들만 골라 생성자를 만들기 때문이다.
따라서 final 키워드를 사용하여 repository를 불러오니 에러가 해결되었다.
OauthId (복합키 설정)
package com.cm.personalProject.domain;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OauthId implements Serializable {
private static final long serialVersionUID = 1L;
private String oauthtype;
private String oauthtoken;
}
LoginController
package com.cm.personalProject.controller;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.cm.personalProject.service.KakaoAPI;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Log4j2
@AllArgsConstructor
@RequestMapping(value = "/social")
@Controller
public class LoginController {
private KakaoAPI kakao;
@GetMapping("/loginPage")
public void getLoginPage() {
}
@GetMapping("/login")
public String login(@RequestParam("code") String code) {
System.out.println(code);
String access_token = kakao.getAccessToken(code);
System.out.println("controller access_token : " + access_token);
String userInfo = kakao.getUserInfo(access_token);
if ("success".equals(userInfo)) {
return "redirect:/home";
} else {
return "redirect:/social/loginPage";
}
}
}
User
package com.cm.personalProject.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import javax.persistence.Transient;
import com.cm.personalProject.domain.OauthId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table
@Data
@AllArgsConstructor
@NoArgsConstructor
@IdClass(OauthId.class)
public class User implements Serializable {
@Transient
private static final long serialVersionUID = 1L;
@Id
private String oauthtype;
@Id
private String oauthtoken;
private String username;
private String useremail;
}
UserRepository
package com.cm.personalProject.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.cm.personalProject.domain.OauthId;
import com.cm.personalProject.entity.User;
public interface UserRepository extends JpaRepository<User, OauthId> {
}
.
.
.
팀 프로젝트 당시에 소셜 로그인을 구현하지 못해서 아쉬움이 많이 남았는데
개인 프로젝트에서 구현하게 되어 뿌듯하다.
이제 구글과 네이버를 통한 소셜 로그인도 구현할 계획이다.