創建一個只能由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
非常快。从感觉上来看,启动速度相当快。

选 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>
);
}
试着动一下
如果是组织外部账户的情况


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

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