[Flutter] Client Credentials Grant 2 - 카카오 로그인

류재성's avatar
Jun 07, 2024
[Flutter] Client Credentials Grant 2 - 카카오 로그인
 

1. 초기화하기

notion image
 
main.dart
import 'package:kakao_flutter_sdk_common/kakao_flutter_sdk_common.dart'; void main() { ... // 웹 환경에서 카카오 로그인을 정상적으로 완료하려면 runApp() 호출 전 아래 메서드 호출 필요 WidgetsFlutterBinding.ensureInitialized(); // runApp() 호출 전 Flutter SDK 초기화 KakaoSdk.init( nativeAppKey: '${YOUR_NATIVE_APP_KEY}', javaScriptAppKey: '${YOUR_JAVASCRIPT_APP_KEY}', ); runApp(MyApp()); ... }
 
notion image
 
네이티브 앱키는 내 애플리케이션 - 앱 키에서 확인할 수 있다.
 
💡
앱 키에서 확인한 네이티브 앱키를 카카오의 main.dart와 AndroidManifest.xml 의 activity 내부에 넣는다. 원래는 변수로 넣어야하지만 테스트를 위해 임시로 넣어둔다.
notion image
notion image
 
 

2. 카카오 로그인(토큰 받기)

 
notion image
 
try { OAuthToken token = await UserApi.instance.loginWithKakaoTalk(); print('카카오톡으로 로그인 성공 ${token.accessToken}'); } catch (error) { print('카카오톡으로 로그인 실패 $error'); }
 
login_page.dart
import 'package:flutter/material.dart'; import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("카카오 로그인"), ), body: ElevatedButton( child: Text("카카오 로그인"), onPressed: () async { try { OAuthToken token = await UserApi.instance.loginWithKakaoTalk(); print('카카오톡으로 로그인 성공 ${token.accessToken}'); } catch (error) { print('카카오톡으로 로그인 실패 $error'); } }, ), ); } }
notion image
 
notion image
 
notion image
 
버튼을 누르면 카카오 페이지로 넘어간다. 로그인이 정상적으로 되면 토큰을 받을 수 있다.
 
 

3. 서버와 통신하기

💡
애플리케이션과 카카오 서버 간 통신을 통해 받은 토큰을 스프링 서버로 보낸다. 스프링 서버는 받은 토큰을 카카오와 통신을 통해 검증한다.
 

3.1 플러터 통신 코드 만들기

💡
플러터에서 http 통신을 쉽게 하기 위해 dio 라이브러리를 설치한다.
 
pubspec.yml
notion image
 
http.dart
import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; final baseUrl = "http://192.168.0.171:8080"; // mac : ipconfig getifaddr en0 final dio = Dio( BaseOptions( baseUrl: baseUrl, // 내 IP 입력 contentType: "application/json; charset=utf-8", validateStatus: (status) => true, // 200 이 아니어도 예외 발생안하게 설정 ), ); const secureStorage = FlutterSecureStorage(); // 받은 토큰을 디바이스에 저장한다.
 
login_page.dart
import 'package:flutter/material.dart'; import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart'; import 'package:oauthapp/_core/http.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("카카오 로그인"), ), body: ElevatedButton( child: Text("카카오로그인"), onPressed: () async { kakaoLogin(); }, ), ); } void kakaoLogin() async { try { // 1. 크리덴셜 로그인 - 토큰 받기 OAuthToken token = await UserApi.instance.loginWithKakaoAccount(); // Bl_yCbOpzJe4vQGNCBX_cQI0VVBvdUm3AAAAAQo9dNsAAAGP3IxzfMYNwJ_muSR4 print('카카오계정으로 로그인 성공 ${token.accessToken}'); // 2. 토큰(카카오)을 스프링서버에 전달하기 (스프링 서버한테 나 인증했어!! 라고 알려주는것) await dio.get("/oauth/callback", queryParameters: {"accessToken": token.accessToken}); // 3. 토큰(스프링서버) 응답받기 // 4. 시큐어 스토리지에 저장 } catch (error) { print('카카오계정으로 로그인 실패 $error'); } } }
 

3.2 스프링 컨트롤러 만들기

 
userController
@GetMapping("/oauth/callback") public ResponseEntity<?> oauthCallBack(String accessToken) { System.out.println("스프링 서버가 받은 카카오 토큰: " +accessToken); return ResponseEntity.ok(new ApiUtil(null)); }
 
notion image
notion image
 
카카오에서 받은 토큰을 스프링 서버로 전송 완료되었다.
 

4. 스프링 서버에서 JWT 토큰 생성하기

💡
카카오에서 받은 토큰을 사용하게 되면 매번 카카오를 통해 검증을 해야 한다. 따라서 애플리케이션을 통해 받은 토큰을 카카오에게 검증한 후 OAuth 가 완료되면 스프링 서버가 클라이언트에게 토큰을 발급해준다. 그럼 앱은 새롭게 받은 토큰을 통해 서버와 통신할 수 있다.
 

4.1 카카오로그인 Service 레이어 만들기

 
💡
서비스 레이어의 역할은 다음과 같다. 1. 카카오에서 사용자 정보 요청하기
  1. 카카오를 통해 받은 데이터를 DB에서 조회, 데이터가 있으면 로그인, 없으면 강제 회원가입
  1. JWT 생성 후 앱에게 전달하기
 
UserService
import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import shop.mtcoding.blog._core.errors.exception.Exception400; import shop.mtcoding.blog._core.errors.exception.Exception401; import shop.mtcoding.blog._core.utils.JwtUtil; import java.util.Optional; import java.util.UUID; @RequiredArgsConstructor @Service // IoC 등록 public class UserService { @Transactional public String 카카오로그인(String kakaoAccessToken) { // 1. RestTemplate 객체 생성 RestTemplate rt = new RestTemplate(); // 2. 토큰으로 사용자 정보 받기 (PK, Email) HttpHeaders headers = new HttpHeaders(); headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); headers.add("Authorization", "Bearer "+kakaoAccessToken); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers); ResponseEntity<KakaoResponse.KakaoUserDTO> response = rt.exchange( "https://kapi.kakao.com/v2/user/me", HttpMethod.GET, request, KakaoResponse.KakaoUserDTO.class); // 3. 해당정보로 DB조회 (있을수, 없을수) String username = "kakao_"+response.getBody().getId(); User userPS = userJPARepository.findByUsername(username) .orElse(null); // 4. 있으면? - 조회된 유저정보 리턴 if(userPS != null){ return JwtUtil.create(userPS); }else{ // 5. 없으면? - 강제 회원가입 User user = User.builder() .username(username) .password(UUID.randomUUID().toString()) .email(response.getBody().getProperties().getNickname()+"@nate.com") .provider("kakao") .build(); User returnUser = userJPARepository.save(user); return JwtUtil.create(returnUser); } } }
 

4.2 ResponseDTO 만들기

💡
앱을 통해 받은 토큰을 카카오에 검증 후, 검증이 완료되면 받을 회원 정보를 담는다. import java.sql.Timestamp; Timestamp 의 임포트가 달라지면 오류가 나니 주의하
 
KakaoResponse
package shop.mtcoding.blog.user; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.sql.Timestamp; public class KakaoResponse { @Data public static class KakaoUserDTO { private Long id; @JsonProperty("connected_at") private Timestamp connectedAt; private Properties properties; @Data class Properties { private String nickname; } } }
 

4.3 User 엔티티

💡
provider 필드가 필요하다. 카카오, 네이버 등 다양한 OAuth 인증을 위해 사용된다.
User
package shop.mtcoding.blog.user; import jakarta.persistence.*; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import java.sql.Timestamp; @NoArgsConstructor @Data @Table(name = "user_tb") @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true) private String username; private String password; private String email; private String provider ; // 카카오 네이버 등 구분 @CreationTimestamp // pc -> db (날짜주입) private Timestamp createdAt; @Builder public User(Integer id, String username, String password, String email, String provider, Timestamp createdAt) { this.id = id; this.username = username; this.password = password; this.email = email; this.provider = provider; this.createdAt = createdAt; } }
 
 

4.5 컨트롤러 만들기

💡
스프링 서버에서 새롭게 생성된 토큰을 앱에 전달한다.
UserController
@GetMapping("/oauth/callback") public ResponseEntity<?> oauthCallback(@RequestParam("accessToken") String kakaoAccessToken){ System.out.println("스프링에서 받은 카카오토큰 : "+kakaoAccessToken); String blogAccessToken = userService.카카오로그인(kakaoAccessToken); return ResponseEntity.ok().header("Authorization", "Bearer "+blogAccessToken).body(new ApiUtil(null)); }
 
 

5. 앱에서 토큰 저장하기

💡
스프링 서버에서 새롭게 받은 토큰을 시큐어 스토리지에 저장한다.
 
login_page.dart
import 'package:flutter/material.dart'; import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart'; import 'package:oauthapp/_core/http.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("카카오 로그인"), ), body: ElevatedButton( child: Text("카카오로그인"), onPressed: () async { kakaoLogin(); }, ), ); } void kakaoLogin() async { try { // 1. 크리덴셜 로그인 - 토큰 받기 OAuthToken token = await UserApi.instance.loginWithKakaoAccount(); // Bl_yCbOpzJe4vQGNCBX_cQI0VVBvdUm3AAAAAQo9dNsAAAGP3IxzfMYNwJ_muSR4 print('카카오계정으로 로그인 성공 ${token.accessToken}'); // 2. 토큰(카카오)을 스프링서버에 전달하기 (스프링 서버한테 나 인증했어!! 라고 알려주는것) final response = await dio.get("/oauth/callback", queryParameters: {"accessToken": token.accessToken}); // 3. 토큰(스프링서버) 응답받기 final blogAccessToken = response.headers["Authorization"]!.first; print("blogAccessToken : ${blogAccessToken}"); // 4. 시큐어 스토리지에 저장 secureStorage.write(key: "blogAccessToken", value: blogAccessToken); } catch (error) { print('카카오계정으로 로그인 실패 $error'); } } }
 
notion image
 
스프링 서버가 발급받은 토큰을 확인할 수 있다.
notion image
 
회원가입도 완료되었다.
Share article

{CODE-RYU};