創建一個只能由Google認證的組織內帳戶使用的服務

你好!最近我参与了一个React的项目,为了学习,我决定尝试做点东西。
毕竟自己动手创作是最好的方式!

想做的事情 zuò de

    • Google認証

 

    • 組織内のみ、認証を通す

 

    認証が通ったらダッシュボードに遷移する

环境建设

创建React开发环境

由于以前一直只使用create-react-app,所以我尝试使用了vite。

$ npm create vite@latest test_project
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
    Vue
❯   React
    Preact
    Lit
    Svelte
    Solid
    Qwik
    Others

我会选择React作为框架,并尝试选择”TypeScript + SWC”作为变种。SWC是基于Rust的Web平台,据说它的编译速度比vite的常规版本(Babel)快20倍以上。

$ npm create vite@latest test_project
✔ Select a framework: › React
? Select a variant: › - Use arrow-keys. Return to submit.
    TypeScript
❯   TypeScript + SWC
    JavaScript
    JavaScript + SWC

按照以下指示执行命令。

$ npm create vite@latest test_project
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

Scaffolding project in /Users/xxx/dev/test_project...

Done. Now run:

  cd test_project
  npm install
  npm run dev

首先运行npm install。这是很常见的操作。

$ npm install

added 152 packages, and audited 153 packages in 31s

37 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

然后运行npm run。

$ npm run dev

> test_project@0.0.0 dev
> vite


  VITE v4.4.9  ready in 487 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

非常快。从感觉上来看,启动速度相当快。

スクリーンショット 2023-09-02 13.13.42.png

选 TypeScript 会有 SWC 的效果吗?

$ npm run dev

> tsonly@0.0.0 dev
> vite


  VITE v4.4.9  ready in 997 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

由于「TypeScript + SWC」使用了487毫秒,因此看起来有SWC的版本更快。
既然更快更好,我们将继续使用SWC版本。

重要文件

这次根据以下要求来创建。
由于是组织内认证的目的,所以功能将保持简单。

    • 未認証の場合は、認証ボタンが表示されている

 

    認証が成功したら、ダッシュボードに遷移する

请参考这段源代码。

毫無疑問,最好參考官方文件,所以我參考了官方的源代碼。

目录结构

最终的目录结构是这样的。

test_project
├── README.md
├── index.html
├── node_modules
│   └── ....
│   └── ....
├── package-lock.json
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── AuthContext.tsx
│   ├── Dashboard.tsx
│   ├── PrivateRoute.tsx
│   ├── assets
│   │   └── react.svg
│   ├── index.css
│   ├── main.tsx
│   ├── oauthUtils.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

追加的文件

执行与OAuth相关的处理的实用函数

// oauthUtils.ts
export function parseFragmentString(fragmentString: string) {
    const params: { [key: string]: string } = {};
    const regex = /([^&=]+)=([^&]*)/g;
    let m: RegExpExecArray | null;
  
    while ((m = regex.exec(fragmentString)) !== null) {
      params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
    }
    return params;
  }
  
  export function saveOAuthParams(params: { [key: string]: string }) {
    localStorage.setItem('oauth2-params', JSON.stringify(params));
  }
  
  export function getSavedOAuthParams() {
    return JSON.parse(localStorage.getItem('oauth2-params') || '{}');
  }

管理认证状态

// AuthContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface AuthContextProps {
  isAuthenticated: boolean;
  handleAuthSuccess: () => void;
}

const AuthContext = createContext<AuthContextProps | null>(null);

export const useAuth = () => {
    const context = useContext(AuthContext);
    if (!context) {
      throw new Error("useAuth must be used within an AuthProvider");
    }
    return context;
  };
  
interface AuthProviderProps {
  children: ReactNode;
}

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const handleAuthSuccess = () => {
    setIsAuthenticated(true);
  };
  
  return (
    <AuthContext.Provider value={{ isAuthenticated, handleAuthSuccess }}>
      {children}
    </AuthContext.Provider>
  );
};

根据认证状态进行路由。

import React from 'react';
import { useAuth } from './AuthContext';
import { Navigate } from 'react-router-dom';

interface PrivateRouteProps {
  children: React.ReactNode;
}

const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
  const { isAuthenticated } = useAuth();

  return isAuthenticated ? <>{children}</> : <Navigate to="/" />;
};

export default PrivateRoute;

用于仪表盘

import React from 'react';

const Dashboard: React.FC = () => {
  return (
    <div>
      <h1>Dashboard</h1>
      <p>You are authenticated.</p>
    </div>
  );
};

export default Dashboard;

修改后的文件

接下来,我们会修改由Vite创建的文件。
我们会将谷歌认证按钮的CSS代码插入到这里,没有仔细编写。

// App.tsx
import React, { useEffect, useCallback } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Dashboard from './Dashboard';
import { AuthProvider } from './AuthContext';
import PrivateRoute from './PrivateRoute';
import { useAuth } from './AuthContext';
import { useNavigate } from 'react-router-dom';

import { parseFragmentString, saveOAuthParams, getSavedOAuthParams } from './oauthUtils';

const YOUR_CLIENT_ID = 'xxx-xxx.apps.googleusercontent.com';
const YOUR_REDIRECT_URI = 'http://localhost:5173';

type OAuthParams = {
  [key: string]: string;
};

const buttonStyle = {
  backgroundColor: "#4285f4",
  color: "white",
  padding: "10px 20px",
  border: "none",
  borderRadius: "2px",
  fontSize: "18px",
  cursor: "pointer",
  position: "absolute",
  top: "50px",
  left: "50%",
  transform: "translateX(-50%)",
};

const MainApp: React.FC = () => {
  const { handleAuthSuccess, isAuthenticated } = useAuth();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (isAuthenticated) {
      navigate('/dashboard');
    }
  }, [isAuthenticated, navigate]);

  const performGoogleAuth = useCallback(() => {
    const params = getSavedOAuthParams();
    if (params && params['access_token']) {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', `https://www.googleapis.com/drive/v3/about?fields=user&access_token=${params['access_token']}`);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          handleAuthSuccess();
        } else if (xhr.readyState === 4 && xhr.status === 401) {
          oauth2SignIn();
        }
      };
      xhr.send(null);
    } else {
      oauth2SignIn();
    }
  }, [handleAuthSuccess]);

  const oauth2SignIn = () => {
    const oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
    const params: OAuthParams = {
      'client_id': YOUR_CLIENT_ID,
      'redirect_uri': YOUR_REDIRECT_URI,
      'scope': 'https://www.googleapis.com/auth/drive.metadata.readonly',
      'state': 'perform_google_auth',
      'include_granted_scopes': 'true',
      'response_type': 'token'
    };

    const form = document.createElement('form');
    form.setAttribute('method', 'GET');
    form.setAttribute('action', oauth2Endpoint);

    for (const p in params) {
      const input = document.createElement('input');
      input.setAttribute('type', 'hidden');
      input.setAttribute('name', p);
      input.setAttribute('value', params[p]);
      form.appendChild(input);
    }

    document.body.appendChild(form);
    form.submit();
  };

  useEffect(() => {
    function handleHashChange() {
      const fragmentString = window.location.hash.substring(1);
      const params = parseFragmentString(fragmentString);
  
      if (Object.keys(params).length > 0) {
        saveOAuthParams(params);
        if (params['state'] && params['state'] === 'perform_google_auth') {
          performGoogleAuth();
        }
      }
    }
  
    handleHashChange();
  
    window.addEventListener('hashchange', handleHashChange);
  
    return () => {
      window.removeEventListener('hashchange', handleHashChange);
    };
  }, [performGoogleAuth]);
  
  const Home: React.FC = () => (
    <button style={buttonStyle} onClick={performGoogleAuth}>Google認証</button>
  );

  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route 
        path="/dashboard" 
        element={<PrivateRoute><Dashboard /></PrivateRoute>} 
      />
    </Routes>
  );
};

const App: React.FC = () => {
  return (
    <AuthProvider>
      <Router>
        <MainApp />
      </Router>
    </AuthProvider>
  );
};

export default App;

请将YOUR_CLIENT_ID设置为您通过官方文档步骤获得的客户端ID。
同时,将YOUR_REDIRECT_URI设置为认证后的重定向URL。由于我们在本地使用vite进行开发,所以将其设置为http://localhost:5173。

// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = document.getElementById('root');
if (root) {
  const appRoot = createRoot(root);
  appRoot.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

试着动一下

如果是组织外部账户的情况

スクリーンショット 2023-09-02 19.23.42.png
スクリーンショット 2023-09-02 19.24.52.png

只要在认证画面设置中指定了“内部”,就可以阻止外部组织的账户。

如果是组织内账户的情况下。

スクリーンショット 2023-09-02 19.32.30.png

总结

这次我们用React进行了组织内的Google认证。
每家公司都有不同的业务,但很难找到与该公司业务内容完全匹配的服务。
仅仅简化认证流程,就可以避免用户管理的麻烦,使开发变得更容易了?

我打算进行更加正式的实现,稍后再通知大家!

bannerAds