ABOUT ME

-

Total
-
  • Rust: 카카오 소셜 로그인 하기 (JWT, actix-rs, react.js)
    컴퓨터/Rust 2023. 7. 10. 18:55
    728x90
    반응형

    mongodb 유저 정보
    카카오톡 프로필 이미지

     

    소개: 이 글에서는 React.js 프트엔드에서 간단하게 카카오 로그인을 구현한 예제이다.

    프론트만으로 해결하려 했으나 redirect 등의 문제로 백엔드를 Rust로 구현하게 되었다. (React.js + Actix-rs)

    왜냐하면 vercel로 배포했기 때문에 프론트만 업로드 되어있다...프론트+백엔드 같이 배포하면 더 쉬워진다.

    백엔드는 fly.io와 같은 사이트를 이용해서 퍼블릭 주소로 서비스 해준다.

     

    일단 https://developers.kakao.com/console/app 에서 앱을 만들고 카카오 로그인 기능을 켜준다.

    그다음 아래에 REDIRECT URI에 개발 홈페이지 주소를 적어준다.

     

    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

     

    React.js

    프론트엔드에서는 일단 카카오 REST 키를 이용해서 할 것이다. 다른 방법들도 있으니 그건 인터넷 참고

    아바타 버튼을 누르면 handleLoginOpen 함수를 불러서 rest api 키와 리다이렉트 경로를 지정해서 넘어가준다.

    일단 넘어가줘야 한다. 그냥 fetch로 하면 OAuth2 방식이므로 잘 안된다.

    const handleLoginOpen = () => {
        const rest_api_key = process.env.REACT_APP_KAKAO_REST_KEY; //REST API KEY
        const redirect_uri = "http://나의 리다이렉트 경로/oauth"; //Redirect URI
    
        // oauth 요청 URL
        const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${rest_api_key}&redirect_uri=${redirect_uri}&response_type=code`;
    
        window.location.href = kakaoURL; // redirect user to Kakao login page
      };
      
      ...
      
      <Link to="#" onClick={handleLoginOpen}>
        <Avatar />
      </Link>

     

    리다이렉트 경로는 백엔드를 연결했다. (로그인 -> 백엔드에서 -> access_token 얻기 -> 유저 정보 얻기 -> mongodb에 유저 정보 저장하기 -> JWT 토큰 만들어서 프론트 주소로 재경로 -> 프론트에서 oauth?token=... 데이터를 로컬에 저장)

     

    Rust

    몽고DB에 연결하고 kauth.kakao.com에 맞는 파라미터를 만들고 요청하고 JWT 토큰을 만드는 그러한 코드이다.

    #[get("/oauth")]
    async fn handle_auth(conn: web::Data<DbPool>, data: web::Query<AuthRequest>) -> impl Responder {
        let code = &data.code;
        println!("{}", code);
        let users = conn
            .lock()
            .unwrap()
            .database("libra")
            .collection::<User>("users");
    
        // Here, use the code to request an access token from Kakao API
    
        let client = reqwest::Client::new();
        let params = [
            ("grant_type", "authorization_code"),
            ("client_id", KAKAO_REST_KEY),
            ("redirect_uri", REDIRECT_URI),
            ("code", &code),
        ];
    
        let res = client
            .post("https://kauth.kakao.com/oauth/token")
            .header(
                "Content-Type",
                "application/x-www-form-urlencoded; charset=UTF-8",
            )
            .form(&params)
            .send()
            .await
            .unwrap();
    
        // let text = res.text().await.unwrap();
        // println!("Response: {}", text);
    
        let kakao_response: KakaoResponse = res.json().await.unwrap();
    
        let access_token = kakao_response.access_token;
    
        println!("Access token: {}", access_token);
    
        // Here, use the access token to request user information from Kakao API
        let res = client
            .get("https://kapi.kakao.com/v2/user/me")
            .bearer_auth(&access_token)
            .send()
            .await
            .unwrap();
    
        // let text = res.text().await.unwrap();
        // println!("Response2: {}", text);
    
        let user_info: User = res.json().await.unwrap();
    
        // create a new user document
        let user_doc = doc! {
            "$set": {
                "access_token": &access_token,
                // add other fields as needed
            }
        };
    
        // update the user document in the collection, or insert if it does not exist
        users
            .update_one(
                doc! { "id": &user_info.id, "connected_at": &user_info.connected_at },
                user_doc,
                UpdateOptions::builder().upsert(true).build(),
            )
            .await
            .unwrap();
    
        // Here, generate a JWT and send it to the front end
        let my_claims = Claims {
            sub: user_info.id, // user identifier
            exp: (SystemTime::now()
                + Duration::from_secs(kakao_response.refresh_token_expires_in as u64)) // expires
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs() as usize,
        };
    
        let key = EncodingKey::from_secret(SECRET_KEY.as_ref());
    
        let token: String = encode(&Header::default(), &my_claims, &key).unwrap();
    
        // HttpResponse::Ok().json(json!({ "token": token }))
        let redirect_url = format!("http://내 프론트 경로/oauth?token={}", token);
    
        // redirect the client to the URL
        HttpResponse::Found()
            .append_header(("Location", redirect_url))
            .finish()
    }

     

    React.js

    react-router-dom v6+ 기준으로 oauth 링크를 만들어 주었다. (백엔드에서 프론트주소/oauth?token=... 으로 이동하기에)

    oauth로 하면 catch all이다, regex 표현으로 저기에 대충 맞으면 다 저기로 넘어간다.

     

    App.js

    import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
    
    import Home from "./pages/Home/Home";
    import HandleAuth from "./pages/HandleAuth/HandleAuth";
    import SearchResult from "./pages/SearchResult/SearchResult";
    
    import "./App.css";
    
    function App() {
      return (
        <div className="app">
          <Router>
            <Routes>
              {/* Home (the one with the search on) */}
              <Route path="/" element={<Home />} />
    
              {/* SearchPage (The results page) */}
              <Route path="search" element={<SearchResult />} />
    
              <Route path="oauth" element={<HandleAuth />} />
            </Routes>
          </Router>
        </div>
      );
    }
    
    export default App;

     

    HandleAuth.js

    oauth에서 로컬에 저장하고 홈 (/)으로 이동한다.

    import { useEffect } from "react";
    import { useNavigate, useLocation } from "react-router-dom";
    
    const HandleAuth = () => {
      const location = useLocation();
      const navigate = useNavigate();
    
      useEffect(() => {
        // extract the token from the URL
        const queryParams = new URLSearchParams(location.search);
        const token = queryParams.get("token");
    
        if (token) {
          // save the token in local storage
          localStorage.setItem("jwt", token);
          console.log(token);
    
          // redirect the user to the Home page
          navigate("/");
        } else {
          console.error("No token in URL");
        }
      }, [location, navigate]);
    
      return null; // or you can return a loading spinner or a message while the token is being fetched
    };
    
    export default HandleAuth;

     

    끝이다.

    일단 kauth 페이지들은 CORS가 열려있어서 모든 호출이 가능하고 (credentials이 필요 없다는 이야기)

    인가요청은 카카오 측 페이지로 이동하고 리다이렉트 해서 AJAX 방식 (fetch()...)로 하면 안 된다.

    (로그인 이후 -> 리다이렉트 URI에서 token 조회는 fetch로 가능)

    JS SDK의 Kakao.Auth.login 방식은 팝업을 띄우고 팝업 내 통신으로 별도 토큰 요청 없이, 토큰까지 리턴 받는 방식이 있는데 그건 SDK를 참고한다.

     

    728x90

    댓글