카카오 OAuth(코드 방식) 실습

박선규's avatar
Jun 05, 2024
카카오 OAuth(코드 방식) 실습
Contents
OAuth

OAuth

notion image
 
 

구조 만들기

notion image
notion image
server: servlet: encoding: charset: utf-8 force: true port: 8080 spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:test;MODE=MySQL username: sa password: h2: console: enabled: true jpa: hibernate: ddl-auto: create # none, update show-sql: true properties: hibernate: format_sql: true
 
 

엔티티

notion image
package shop.mtcoding.loginapp.user; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor @Getter @Table(name = "user_tb") @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String username; private String password; private String email; private String provider; // facebook, kakao, apple, naver (OAuth 제공자) @Builder public User(Integer id, String username, String password, String email, String provider) { this.id = id; this.username = username; this.password = password; this.email = email; this.provider = provider; } }
 
 
 
notion image
package shop.mtcoding.loginapp.shop; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor @Getter @Table(name = "Shop_tb") @Entity public class Shop { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String name; //상품명 private String price; //상품 가격 private String qty; //재고 @Builder public Shop(Integer id, String name, String price, String qty) { this.id = id; this.name = name; this.price = price; this.qty = qty; } }
 
 
 
리퀘스트랑 세션에 접근하려면 야물에서 이거 설정해야됨
리퀘스트랑 세션에 접근하려면 야물에서 이거 설정해야됨
 
 
sql:더미 데이터 설정
sql:더미 데이터 설정
 

Repository

notion image
package shop.mtcoding.loginapp.user; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; public interface UserRepository extends JpaRepository<User, Integer> { User findByUsername(@Param("username")String username); }

service

package shop.mtcoding.loginapp.user; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public void 회원가입(String username, String password, String email){ User user = User.builder() .username(username) .password(password) .email(email) .build(); userRepository.save(user); } public User 로그인(String username, String password){ User user = userRepository.findByUsername(username); if(user == null){ throw new RuntimeException("아이디가 없습니다."); }else{ if (user.getPassword().equals(password)){ return user; }else{ throw new RuntimeException("비밀번호가 틀렸습니다."); } } } }
 
 

UserController

package shop.mtcoding.loginapp.user; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @RequiredArgsConstructor @Controller public class UserController { private final UserService userService; private final HttpSession session; //Ioc 등록 돼있음 @GetMapping("/join-form") public String joinForm() { return "join-form"; } @GetMapping("/login-form") public String loginForm() { return "login-form"; } @PostMapping("/join") public String join(String username, String password, String email) { userService.회원가입(username, password, email); return "redirect:/login-form"; } @PostMapping("/login") public String login(String username, String password) { User sessionUser = userService.로그인(username, password); session.setAttribute("sessionUser", sessionUser); return "redirect:/shop"; } }
 

ShopService

package shop.mtcoding.loginapp.shop; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @RequiredArgsConstructor @Service public class ShopService { private final ShopRepository shopRepository; public List<Shop> 상품목록() { List<Shop> shopList = shopRepository.findAll(); return shopList; } }
 

코드방식

임시코드를 브라우저가 받아서 서버에전달한다.
사용자가 서버에 전달할줄 모르니까 OAuth 제공자가 302 로케이션이랑 같이 제공해주면서 자동으로 리다이렉션 하게끔 해준다.
 
그 회사의 pk + 프로바이더를 넣는다
 
OAuth 로그인 만으로 정보가 충족하지 않아 추가정보가 필요한데 OAuth로그인후에 바로 추가적인 정보를 입력하는(ex:주소) ux가 나오면 별로다. 추후에 배송할 때 받을 수 도 있다.
 
 
 
 
 
 
인증은 로그인 성공 했을 때 인가: 권한 (페이지 접근, 수정)
인증은 로그인 성공 했을 때 인가: 권한 (페이지 접근, 수정)
 
notion image

jwt (Access Token, Refresh Token)

결론:jwt를 발급 받으면 Access Token이랑 Refresh Token이 발급되는데 Access Token은 사용자의 정보가 담겨있는 토큰이고 Refresh Token은 Access Token의 유효기간이 만료 됐을 때 재발급 해주는 역할을 하는 토큰 입니다.
 
서비스 로그인 과정 순서
로그인 요청 → 인증 및 동의 요청 → 인가 코드 발급 → 토큰 발급
 

앱 설정

notion image
 
 
 
notion image
notion image
각 플랫폼에 맞게 등록 플러터는:안드로이드.ios
각 플랫폼에 맞게 등록 플러터는:안드로이드.ios
나중에 배포하면 도메인 주소로
나중에 배포하면 도메인 주소로
notion image
notion image
임시코드를 받을 만들어뒀던 url 입력
임시코드를 받을 만들어뒀던 url 입력
 
저게 사업자번호 등록인데 나는 사업자가 아니므로 못함
저게 사업자번호 등록인데 나는 사업자가 아니므로 못함
 
그래서 스코프 설정만 추가
notion image
스코프가 추가됨
스코프가 추가됨
 
 

인가 코드 받기

notion image
notion image
notion image
notion image
내 API 키 넣기
내 API 키 넣기
notion image
notion image
notion image
첫번재 302 코드에 적혀있는 로케이션 주소를 보고 302를 준거다.
카카오측에서 이미 앱애 등록 돼있는데 URI 주소를 달라고 한 이유는?
-안받아도 되지만 검증하기 위해 받은거다.
첫번재 302 코드에 적혀있는 로케이션 주소를 보고 302를 준거다. 카카오측에서 이미 앱애 등록 돼있는데 URI 주소를 달라고 한 이유는? -안받아도 되지만 검증하기 위해 받은거다.
notion image
 

rfc 문서 읽기

notion image
notion image
 
 

토큰 받기

notion image
notion image
notion image
 

사용자 정보 받기

notion image
notion image
notion image
 
 
notion image
클라이언트가 서버에 직접 던지는 정보를 신뢰 할 수 없다.
검증을 해봐야 한다.
 

DTO만들기

스코프 설정을 내가 어떻게 했냐에 따라 값이 배열이 될 수 도있으니 값 확인 잘하고 DTO만들기
스코프 설정을 내가 어떻게 했냐에 따라 값이 배열이 될 수 도있으니 값 확인 잘하고 DTO만들기
 

JsonProperty 라이브러리

@JsonProperty 어노테이션은 Jackson 라이브러리에서 사용되는 어노테이션으로, JSON 데이터와 Java 객체 간의 매핑을 커스터마이징할 때 사용됩니다. 이 어노테이션을 통해 필드나 메서드의 JSON 프로퍼티 이름을 명시적으로 지정할 수 있습니다. 주로 직렬화(Serialization)와 역직렬화(Deserialization) 과정에서 JSON 필드 이름을 Java 객체 필드 이름과 다르게 지정해야 할 때 유용합니다.

주요 기능

  1. JSON 필드 이름 지정: JSON의 필드 이름과 Java 객체의 필드 이름이 다를 때 이를 매핑.
  1. 직렬화 제어: 특정 필드를 JSON 출력에 포함시키거나 제외.
  1. 역직렬화 제어: JSON 입력 시 특정 필드를 Java 객체에 매핑하거나 제외.

사용 예시

1. JSON 필드 이름 지정

JSON의 필드 이름이 Java 객체의 필드 이름과 다를 때 사용합니다.
import com.fasterxml.jackson.annotation.JsonProperty; public class User { @JsonProperty("user_name") private String userName; @JsonProperty("user_age") private int age; // Getter와 Setter 생략 }
이 경우, JSON 데이터가 다음과 같이 매핑됩니다:
{ "user_name": "John", "user_age": 30 }

2. 직렬화 및 역직렬화 제어

필드를 직렬화하거나 역직렬화할 때 제어할 수 있습니다.
import com.fasterxml.jackson.annotation.JsonProperty; public class User { private String userName; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; // Getter와 Setter 생략 }
위의 예제에서 password 필드는 JSON 역직렬화 시에만 사용되고 직렬화 시에는 제외됩니다.
 
notion image
 
 

카카오로그인 Service

public User 카카오로그인(String code) { // 1. code로 카카오에서 토큰 받기 (위임완료) - oauth2.0 // 1.1 RestTemplate 설정 RestTemplate rt = new RestTemplate(); // 1.2 http header 설정 HttpHeaders headers = new HttpHeaders(); headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 1.3 http body 설정 MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("grant_type", "authorization_code"); body.add("client_id", "3d03bec0f109fbfea32461ee7fd4f9a0"); body.add("redirect_uri", "http://localhost:8080/oauth/callback"); body.add("code", code); // 1.4 body+header 객체 만들기 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers); // 1.5 api 요청하기 (토큰 받기) ResponseEntity<KakaoResponse.TokenDTO> response = rt.exchange( "https://kauth.kakao.com/oauth/token", HttpMethod.POST, request, KakaoResponse.TokenDTO.class); // 1.6 값 확인 System.out.println(response.getBody().toString()); // 2. 토큰으로 사용자 정보 받기 (PK, Email) HttpHeaders headers2 = new HttpHeaders(); headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); headers2.add("Authorization", "Bearer "+response.getBody().getAccessToken()); HttpEntity<MultiValueMap<String, String>> request2 = new HttpEntity<>(headers2); ResponseEntity<KakaoResponse.KakaoUserDTO> response2 = rt.exchange( "https://kapi.kakao.com/v2/user/me", HttpMethod.GET, request2, KakaoResponse.KakaoUserDTO.class); System.out.println("response2 : "+response2.getBody().toString()); // 3. 해당정보로 DB조회 (있을수, 없을수) String username = "kakao_"+response2.getBody().getId(); User userPS = userRepository.findByUsername(username); // 4. 있으면? - 조회된 유저정보 리턴 if(userPS != null){ System.out.println("어? 유저가 있네? 강제로그인 진행"); return userPS; }else{ System.out.println("어? 유저가 없네? 강제회원가입 and 강제로그인 진행"); // 5. 없으면? - 강제 회원가입 // 유저네임 : (provider_pk) // 비밀번호 : UUID // 이메일 : email 받은 값 // 프로바이더 : kakao User user = User.builder() .username(username) .password(UUID.randomUUID().toString()) .email(response2.getBody().getProperties().getNickname()+"@nate.com") .provider("kakao") .build(); User returnUser = userRepository.save(user); return returnUser; } }
notion image
notion image
 
Share article

p4rksk