15주차 스마트디바이스 (토양,카카오톡 실습) - jungjaeyeol/jyeol03 GitHub Wiki

🔐 OAuth 2.0 개념 정리

OAuth 2.0은 제3자 애플리케이션이 사용자 비밀번호를 직접 다루지 않고, 사용자의 자원(예: 구글 계정 정보, 사진 등)에 안전하게 접근할 수 있도록 해주는 권한 부여 프로토콜입니다.

주로 "소셜 로그인", "API 인증", "SSO" 등에 사용됩니다.


📦 구성 요소 (Components)

구성 요소 설명
Resource Owner 자원의 소유자 = 사용자
Client 사용자 자원에 접근하려는 앱 (예: 쇼핑몰 앱, 메신저 등)
Authorization Server 사용자 인증 및 토큰 발급 (예: Google OAuth 서버)
Resource Server 보호된 리소스가 저장된 서버 (예: Google Drive API)
Access Token 리소스 접근을 위한 권한 증명 수단

🔁 OAuth 2.0 동작 흐름 (Authorization Code Grant 기준)

  1. 사용자가 클라이언트를 통해 로그인 요청
  2. 클라이언트는 사용자를 Authorization Server로 리디렉션
  3. 사용자는 로그인하고 권한을 동의
  4. Authorization ServerAuthorization Code를 클라이언트에 전달
  5. 클라이언트는 이 코드를 이용해 Access Token 요청
  6. Authorization ServerAccess Token 발급
  7. 클라이언트는 이 토큰으로 Resource Server에 접근하여 데이터 요청
[사용자]
    ↓ 로그인 및 동의
[Authorization Server] --(인가 코드)--> [Client]
[Client] --(인가 코드로 토큰 요청)--> [Authorization Server]
[Authorization Server] --(Access Token 발급)--> [Client]
[Client] --(토큰으로 요청)--> [Resource Server]
[Resource Server] --> 보호된 사용자 데이터 반환

🧱 권한 부여 방식 (Grant Types)

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 기기

✅ OAuth 2.0의 장점 (Advantages)

장점 설명
🔐 보안성 사용자 비밀번호를 제3자 앱에 제공하지 않아도 되어 보안 위험 감소
🔁 SSO (Single Sign-On) 한 번 로그인으로 여러 서비스에 인증 가능
🧩 유연한 권한 관리 토큰에 접근 범위(scope)와 만료 시간 설정 가능
🌐 다양한 환경 지원 웹, 모바일, IoT 등 다양한 플랫폼에서 인증 방식 제공
🧱 표준 프로토콜 다양한 서비스(Google, Kakao, GitHub 등)에서 동일한 방식으로 사용 가능
🔄 권한 철회 및 갱신 용이 사용자는 언제든 권한을 철회하거나 토큰을 재발급할 수 있음

토양 센서 값 보내기

1. 보드 연결


2. 어플리케이션 추가


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

  • 플랫폼 연결 (Web)
  • Web 플랫폼 수정 https://localhost 로 저장하기


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

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 토큰을 발급 받아야함.
⚠️ **GitHub.com Fallback** ⚠️