實戰:如何在React應用中實現「使用GitHub登陸」

我遇到一個場景,該場景須要使用Create-React-App在React 應用中實現「使用Github登陸」功能。雖然這聽起來很簡單,但在嘗試作這件事時,你可能會遇到一些麻煩。所以,本文的目的是提供一個指南,幫助你在你的應用程序中實現這樣的功能。讓咱們如今就開始吧!css

步驟1:在Github上建立OAuth應用

按照此處提供的步驟登陸到你的Github賬戶並建立OAuth應用。注意:對於本例,在建立OAuth應用時,若是你在本地運行應用,能夠將主頁URL設置爲http://localhost:3000/,將受權回調URL設置爲http://localhost:3000/login。在根目錄下建立一個 .env 文件,並設置這些變量:node

REACT_APP_CLIENT_ID=你的Client ID
REACT_APP_CLIENT_SECRET=你的Client Secret
REACT_APP_REDIRECT_URI=http://localhost:3000/login

步驟2:建立React應用

繼續使用你的首選來建立你的react應用程序,在這個例子中,咱們將使用Create-React-App。若是你使用這種模式,你必須刪除index.css、App.css、App.test.js和serviceWorker.js等文件。編輯index.js,確保它看起來像這樣:react

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

另外,編輯App.js,確保它看起來像這樣:git

import React, { createContext, useReducer } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import Login from "./components/Login";
import { initialState, reducer } from "./store/reducer";


export const AuthContext = createContext();

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AuthContext.Provider
      value={{
        state,
        dispatch
      }}
    >
    <Router>
      <Switch>
        <Route path="/login" component={Login}/>
        <Route path="/" component={Home}/>
      </Switch>
    </Router>
    </AuthContext.Provider>
  );
}

export default App;

在App.js文件中,導入2個組件(Home.jsLogin.js )。要建立這2個組件,進入src文件夾,建立一個名爲component的文件夾,裏面有2個文件(Home.js和Login.js)。在根文件夾中,你能夠在下面運行此命令來建立它們。github

mkdir -p src/components && cd src/components && touch Home.js Login.js

接下來,你會觀察到,咱們從store導入了狀態和reducer,繼續並設置一個簡單的store,它將保持應用程序狀態。要作到這一點,導航到src文件夾中,並建立一個名爲store的文件夾,在它裏面建立一個名爲reducer的子文件夾,並在reducer文件夾裏面建立一個index.js文件。在根目錄下,你能夠運行下面這個命令來建立它們。shell

mkdir -p src/store/reducer && cd src/store/reducer && touch index.js

Store中index.js文件的內容應以下所示。express

export const initialState = {
  isLoggedIn: JSON.parse(localStorage.getItem("isLoggedIn")) || false,
  user: JSON.parse(localStorage.getItem("user")) || null,
  client_id: process.env.REACT_APP_CLIENT_ID,
  redirect_uri: process.env.REACT_APP_REDIRECT_URI,
  client_secret: process.env.REACT_APP_CLIENT_SECRET,
  proxy_url: process.env.REACT_APP_PROXY_URL
};

export const reducer = (state, action) => {
  switch (action.type) {
    case "LOGIN": {
      localStorage.setItem("isLoggedIn", JSON.stringify(action.payload.isLoggedIn))
      localStorage.setItem("user", JSON.stringify(action.payload.user))
      console.log(action.payload.isLoggedIn)
      return {
        ...state,
        isLoggedIn: action.payload.isLoggedIn,
        user: action.payload.user
      };
    }
    case "LOGOUT": {
      localStorage.clear()
      return {
        ...state,
        isLoggedIn: false,
        user: null
      };
    }
    default:
      return state;
  }
};

它包含InitialState對象和一個reducer函數,該函數包含派發的動做以突變狀態。json

這時,咱們就能夠在咱們的組件上下功夫了。讓咱們在Login.js上工做,這將是一個簡單的組件,它有一個按鈕,能夠觸發Github API的登陸請求。api

import React, { useState, useEffect, useContext } from "react";
import { Redirect } from "react-router-dom";
import Styled from "styled-components";
import GithubIcon from "mdi-react/GithubIcon";
import { AuthContext } from "../App";


export default function Login() {
  const { state, dispatch } = useContext(AuthContext);
  const [data, setData] = useState({ errorMessage: "", isLoading: false });

  const { client_id, redirect_uri } = state;

  useEffect(() => {
    // After requesting Github access, Github redirects back to your app with a code parameter
    const url = window.location.href;
    const hasCode = url.includes("?code=");

    // If Github API returns the code parameter
    if (hasCode) {
      const newUrl = url.split("?code=");
      window.history.pushState({}, null, newUrl[0]);
      setData({ ...data, isLoading: true });

      const requestData = {
        client_id: state.client_id,
        redirect_uri: state.redirect_uri,
        client_secret: state.client_secret,
        code: newUrl[1]
      };

      const proxy_url = state.proxy_url;

      // Use code parameter and other parameters to make POST request to proxy_server
      fetch(proxy_url, {
        method: "POST",
        body: JSON.stringify(requestData)
      })
        .then(response => response.json())
        .then(data => {
          dispatch({
            type: "LOGIN",
            payload: { user: data, isLoggedIn: true }
          });
        })
        .catch(error => {
          setData({
            isLoading: false,
            errorMessage: "Sorry! Login failed"
          });
        });
    }
  }, [state, dispatch, data]);

  if (state.isLoggedIn) {
    return <Redirect to="/" />;
  }

  return (
    <Wrapper>
      <section className="container">
        <div>
          <h1>Welcome</h1>
          <span>Super amazing app</span>
          <span>{data.errorMessage}</span>
          <div className="login-container">
            {data.isLoading ? (
              <div className="loader-container">
                <div className="loader"></div>
              </div>
            ) : (
              <>
                {
                  // Link to request GitHub access
                }
                <a
                  className="login-link"
                  href={`https://github.com/login/oauth/authorize?scope=user&client_id=${client_id}&redirect_uri=${redirect_uri}`}
                  onClick={() => {
                    setData({ ...data, errorMessage: "" });
                  }}
                >
                  <GithubIcon />
                  <span>Login with GitHub</span>
                </a>
              </>
            )}
          </div>
        </div>
      </section>
    </Wrapper>
  );
}

const Wrapper = Styled.section`
  .container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    font-family: Arial;
    
    > div:nth-child(1) {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
      transition: 0.3s;
      width: 25%;
      height: 45%;
      > h1 {
        font-size: 2rem;
        margin-bottom: 20px;
      }
      > span:nth-child(2) {
        font-size: 1.1rem;
        color: #808080;
        margin-bottom: 70px;
      }
      > span:nth-child(3) {
        margin: 10px 0 20px;
        color: red;
      }
      .login-container {
        background-color: #000;
        width: 70%;
        border-radius: 3px;
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: center;
        > .login-link {
          text-decoration: none;
          color: #fff;
          text-transform: uppercase;
          cursor: default;
          display: flex;
          align-items: center;          
          height: 40px;
          > span:nth-child(2) {
            margin-left: 5px;
          }
        }
        .loader-container {
          display: flex;
          justify-content: center;
          align-items: center;          
          height: 40px;
        }
        .loader {
          border: 4px solid #f3f3f3;
          border-top: 4px solid #3498db;
          border-radius: 50%;
          width: 12px;
          height: 12px;
          animation: spin 2s linear infinite;
        }
        @keyframes spin {
          0% {
            transform: rotate(0deg);
          }
          100% {
            transform: rotate(360deg);
          }
        }
      }
    }
  }
`;

Login.js組件內部,請注意如下重要事項:服務器

  1. 咱們導入並利用AuthContext使Store中的全局狀態和操做可在此組件中使用。
  2. 當用戶點擊「用Github登陸」按鈕時,會向Github API提出請求,對咱們的應用進行受權。若是成功的話,Github就會重定向回咱們的應用(受權回調URL),並在URL中加入「code」。
  3. 咱們利用useEffect hook偵聽此「code」什麼時候可用。而後咱們從url中收集它,使用code和其餘數據,如:client_id,redirect_uri,client_secret,繼續經過咱們的proxy server(代理服務器)向Github APIs發出請求(一個簡單的快遞應用,幫助咱們繞過CORS錯誤)。下一步,我將詳細討論代理服務器。
  4. 若是經過代理服務器的認證返回有效的響應,咱們就會調度「LOGIN」事件,在咱們的存儲中設置用戶數據和isLoggedIn有效載荷。

讓咱們更新Home.js組件以顯示一些用戶數據,例如(頭像,姓名,關注者人數等)

import React, { useContext } from "react";
import { Redirect } from "react-router-dom";
import Styled from "styled-components";
import { AuthContext } from "../App";


export default function Home() {
  const { state, dispatch } = useContext(AuthContext);

  if (!state.isLoggedIn) {
    return <Redirect to="/login" />;
  }

  const { avatar_url, name, public_repos, followers, following } = state.user

  const handleLogout = () => {
    dispatch({
      type: "LOGOUT"
    });
  } 

  return (
    <Wrapper>
      <div className="container">
        <button onClick={()=> handleLogout()}>Logout</button>
        <div>
          <div className="content">
            <img src={avatar_url} alt="Avatar"/>
            <span>{name}</span>
            <span>{public_repos} Repos</span>
            <span>{followers} Followers</span>
            <span>{following} Following</span>
          </div>
        </div>
      </div>
    </Wrapper>
  );
}

const Wrapper = Styled.section`
.container{
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: Arial;
  button{
    all: unset;
    width: 100px;
    height: 35px;
    margin: 10px 10px 0 0;
    align-self: flex-end;
    background-color: #0041C2;
    color: #fff;
    text-align: center;
    border-radius: 3px;
    border: 1px solid #0041C2;
    &:hover{
      background-color: #fff;
      color: #0041C2;
    }
  }
  >div{
    height: 100%;
    width: 100%;
    display: flex;
    font-size: 18px;
    justify-content: center;
    align-items: center;
    .content{
      display: flex;
      flex-direction: column;
      padding: 20px 100px;    
      box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
      width: auto;
  
      img{
        height: 150px;
        width: 150px;
        border-radius: 50%;
      }
  
      >span:nth-child(2){
        margin-top: 20px;
        font-weight: bold;
      }
  
      >span:not(:nth-child(2)){
        margin-top: 8px;
        font-size: 14px;
      }
  
    }
  }
}
`;

步驟3:建立代理服務器

最後一步是建立代理服務器,以幫助咱們繞過CORS錯誤。它將是一個簡單的express應用程序,咱們將在header中啓用「Access-Control-Allow-Origin」。咱們將使用它來轉發請求和接收來自Github API的響應,並將所需的響應發送回客戶端(咱們的React應用程序)。將這些變量添加到.env文件中:

REACT_APP_PROXY_URL=http://localhost:5000/authenticate
SERVER_PORT=5000

在根文件夾中,建立一個名爲server的文件夾,並在其中建立一個index.js文件。

const express = require("express");
const bodyParser = require("body-parser");
const FormData = require("form-data");
const fetch = require("node-fetch");
const { client_id, redirect_uri, client_secret } = require("./config");

const config = require("./config");

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.json({ type: "text/*" }));
app.use(bodyParser.urlencoded({ extended: false }));

// Enabled Access-Control-Allow-Origin", "*" in the header so as to by-pass the CORS error.
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  next();
});

app.post("/authenticate", (req, res) => {
  const { code } = req.body;

  const data = new FormData();
  data.append("client_id", client_id);
  data.append("client_secret", client_secret);
  data.append("code", code);
  data.append("redirect_uri", redirect_uri);

  // Request to exchange code for an access token
  fetch(`https://github.com/login/oauth/access_token`, {
    method: "POST",
    body: data,
  })
    .then((response) => response.text())
    .then((paramsString) => {
      let params = new URLSearchParams(paramsString);
      const access_token = params.get("access_token");

      // Request to return data of a user that has been authenticated
      return fetch(`https://api.github.com/user`, {
        headers: {
          Authorization: `token ${access_token}`,
        },
      });
    })
    .then((response) => response.json())
    .then((response) => {
      return res.status(200).json(response);
    })
    .catch((error) => {
      return res.status(400).json(error);
    });
});

const PORT = process.env.SERVER_PORT || 5000;
app.listen(PORT, () => console.log(`Listening on ${PORT}`));

附加連接:

https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/

https://www.graphql.college/implementing-github-oauth-flow/

源碼

若是你按照上面列出的幾個步驟進行操做,則能夠在應用程序中無縫集成「使用Github登陸」功能。

在此處獲取源代碼:Github

相關文章
相關標籤/搜索