15주차 스마트디바이스 (토양,카카오톡 실습) - jungjaeyeol/jyeol03 GitHub Wiki
OAuth 2.0은 제3자 애플리케이션이 사용자 비밀번호를 직접 다루지 않고, 사용자의 자원(예: 구글 계정 정보, 사진 등)에 안전하게 접근할 수 있도록 해주는 권한 부여 프로토콜입니다.
주로 "소셜 로그인", "API 인증", "SSO" 등에 사용됩니다.
구성 요소 | 설명 |
---|---|
Resource Owner | 자원의 소유자 = 사용자 |
Client | 사용자 자원에 접근하려는 앱 (예: 쇼핑몰 앱, 메신저 등) |
Authorization Server | 사용자 인증 및 토큰 발급 (예: Google OAuth 서버) |
Resource Server | 보호된 리소스가 저장된 서버 (예: Google Drive API) |
Access Token | 리소스 접근을 위한 권한 증명 수단 |
- 사용자가 클라이언트를 통해 로그인 요청
- 클라이언트는 사용자를 Authorization Server로 리디렉션
- 사용자는 로그인하고 권한을 동의
- Authorization Server는 Authorization Code를 클라이언트에 전달
- 클라이언트는 이 코드를 이용해 Access Token 요청
- Authorization Server는 Access Token 발급
- 클라이언트는 이 토큰으로 Resource Server에 접근하여 데이터 요청
[사용자]
↓ 로그인 및 동의
[Authorization Server] --(인가 코드)--> [Client]
[Client] --(인가 코드로 토큰 요청)--> [Authorization Server]
[Authorization Server] --(Access Token 발급)--> [Client]
[Client] --(토큰으로 요청)--> [Resource Server]
[Resource Server] --> 보호된 사용자 데이터 반환
OAuth 2.0에는 다양한 인증/권한 흐름이 존재하며, 각각의 사용 목적과 보안 수준에 따라 선택됩니다.
Grant Type | 설명 | 사용 예시 |
---|---|---|
Authorization Code | 사용자 로그인 후, 인가 코드를 발급받고 서버에서 Access Token을 요청. 가장 보안이 강한 방식 | 웹 애플리케이션 |
Authorization Code + PKCE | Authorization Code 방식에 보안 강화 코드(PKCE)를 추가. 모바일/공용 클라이언트용 | 모바일 앱, SPA |
Client Credentials | 사용자 없이 클라이언트(앱) 자체만으로 인증. 머신 간 통신에 적합 | 백엔드 서비스 간 API 호출 |
Resource Owner Password Credentials | 사용자 ID/PW를 클라이언트가 직접 받아 인증. 보안 취약하여 비추천 | 테스트 용도 또는 내부 시스템 |
Implicit | 클라이언트에 바로 Access Token을 발급. 보안상 매우 취약하여 현재는 사용 권장하지 않음 | (사용 자제) 예전 브라우저 앱 |
Device Code | 입력 장치가 부족한 디바이스(스마트TV 등)에서 사용자 인증에 사용 | 스마트TV, 콘솔, IoT 기기 |
장점 | 설명 |
---|---|
🔐 보안성 | 사용자 비밀번호를 제3자 앱에 제공하지 않아도 되어 보안 위험 감소 |
🔁 SSO (Single Sign-On) | 한 번 로그인으로 여러 서비스에 인증 가능 |
🧩 유연한 권한 관리 | 토큰에 접근 범위(scope)와 만료 시간 설정 가능 |
🌐 다양한 환경 지원 | 웹, 모바일, IoT 등 다양한 플랫폼에서 인증 방식 제공 |
🧱 표준 프로토콜 | 다양한 서비스(Google, Kakao, GitHub 등)에서 동일한 방식으로 사용 가능 |
🔄 권한 철회 및 갱신 용이 | 사용자는 언제든 권한을 철회하거나 토큰을 재발급할 수 있음 |
1. 보드 연결

2. 어플리케이션 추가

- 앱 이름, 회사명, 카테고리 설정하기

- 플랫폼 연결 (Web)

- Web 플랫폼 수정
https://localhost
로 저장하기
-
카카오톡 로그인 Redirect URL
-
활성화 설정 ON 으로 바꾸기
-
Redirect URL 에 "https://www.example.com/oauth" 입력하기

- 동의 항목에서 카카오톡 메세지 전송하기
선택 동의, 동의 목적
설정하기

https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=RESI+API키값&redirect_uri=리다이렉션URL
- 주소 입력 후
code=
이후 복사하기
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id={REST_API_KEY}" \
--data-urlencode "redirect_uri={REDIRECT_URI}" \
-d "code={AUTHORIZATION_CODE}"
-
위 코드를 Access 토큰 발급을 위해서 cmd 명령 프롬프트에 입력하기
-
Access 토큰을 발급 받은 후에 복사하기
-
실습 코드
#include <WiFi.h>
#include <HTTPClient.h>
const char *ssid = "Your_SSID"; // 사용하는 WiFi 네트워크 이름 (SSID)
const char *password = "Your_Password"; // 사용하는 WiFi 네트워크 비밀번호
const String rest_api_key = "REST API KEY";
String access_token = "Access token";
String refresh_token = "Refresh token";
#define MsgSendInterval 3600 // 60 * 60 초, 즉 한시간 간격으로 전송
long timeout = 3600; //시간을 초로 나타냄
int sensorValue = 0;
int sensorPin = 34; // 토양 습도 센서 핀
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.println("Connecting");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.print("\nConnected to WiFi : ");
Serial.println(WiFi.localIP());
}
void loop() {
if (timeout++ > MsgSendInterval) // 1시간(60 * 60)에 1번씩 전송
{
if (isAccessTokenExpired() == true) { //access token 만료 여부 확인
if (update_access_token() == false) { // access token 재발급
Serial.println("Access token update failed");
}
}
sensorValue = analogRead(sensorPin);//토양 센서값 읽기
send_message();
timeout = 0;
}
delay(1000);
}
// str문자열에서 start_string와 end_string사이의 문자열을 추출하는 함수
String extract_string(String str, String start_string, String end_string) {
int index1 = str.indexOf(start_string) + start_string.length();
int index2 = str.indexOf(end_string, index1);
String value = str.substring(index1, index2);
return value;
}
bool isAccessTokenExpired() {
HTTPClient http;
bool returnVal = true;
/*
curl -v -X GET "https://kapi.kakao.com/v1/user/access_token_info" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
*/
if (!http.begin("https://kapi.kakao.com/v1/user/access_token_info")) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
int httpCode = http.GET();
// httpCode will be negative on error
if (httpCode > 0) {
// file found at server
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
String expireT = extract_string(payload, "\"expires_in\":", ",");
Serial.println(expireT.toInt());
if (expireT.toInt() > 0) {
returnVal = false;
} else {
returnVal = true;
}
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return returnVal;
}
void send_message() {
HTTPClient http;
String url = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
/*
template_object={
"object_type": "text",
"text": "텍스트 영역입니다. 최대 200자 표시 가능합니다.",
"link": {
"web_url": "https://developers.kakao.com",
"mobile_web_url": "https://developers.kakao.com"
},
"button_title": "바로 확인"
}
*/
String data = String("template_object={") +
String("\"object_type\": \"text\",") +
String("\"text\": \"") + String("토양 센서 값 :") +
String(sensorValue) + //토양 센서 값
String("\",\"link\": {}}"); //link가 없으면 오류메세지 받음
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
}
http.end();
}
/*
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=${REST_API_KEY}" \
-d "refresh_token=${USER_REFRESH_TOKEN}"
*/
bool update_access_token() {
HTTPClient http;
bool retVal = false;
String url = "https://kauth.kakao.com/oauth/token";
String new_refresh_token = "";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
String data = "grant_type=refresh_token&client_id=" + rest_api_key + "&refresh_token=" + refresh_token;
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
access_token = extract_string(response, "{\"access_token\":\"", "\"");
new_refresh_token = extract_string(response, "\"refresh_token\":\"", "\"");
//만료 1개월전부터 갱신되므로 data가 없을 수도 있음
if (new_refresh_token != "") {
refresh_token = new_refresh_token;
}
retVal = true;
} else {
retVal = false;
}
http.end();
return retVal;
}

- Rist키, Redirect 코드를 발급하고 복사해두지 않으면 실습에 어려움이 있음.
- Redirect 코드 발급 이후 5분 이내로 Access 토큰을 발급 받아야함.