使用 Firebase 为 Next.js 进行身份验证
在我的 Next.js 项目中,我需要添加一些身份验证功能。我决定使用 Firebase 来进行用户管理和数据存储。
我需要的东西:
- 使用 Twitter 进行 OAuth 认证
- 客户端身份验证
- 受保护的页面
- 服务器端身份验证
假设:您已经设置了一个基本的 Next.js 项目,并且在 Firebase 中创建了一个项目。
设置 Firebase
安装 Firebase 的软件包
npm i --save firebase firebase-admin
创建一个env.local文件并添加所有必要的 Firebase 密钥。
NEXT_PUBLIC_FIREBASE_API_KEY=********************
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=***********
NEXT_PUBLIC_FIREBASE_PROJECT_ID=*********
FIREBASE_PRIVATE_KEY=*********************
FIREBASE_CLIENT_EMAIL=*************
FIREBASE_DATABASE_URL=*************
现在我们需要创建一些文件来处理与 Firebase 的连接。
lib/firebase.ts- 处理 OAuth 和维护身份验证。
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/functions';
import 'firebase/firestore';
if (!firebase.apps.length) {
firebase.initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID
});
}
export default firebase;
lib/firebase-admin.ts- 在服务器端验证令牌。
import admin from 'firebase-admin';
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
privateKey: process.env.FIREBASE_PRIVATE_KEY,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL
}),
databaseURL: process.env.FIREBASE_DATABASE_URL
});
}
const db = admin.firestore();
const auth = admin.auth();
export { db, auth };
lib/db.ts数据库查询
import firebase from '../lib/firebase';
const firestore = firebase.firestore();
export function updateUser(uid: string, data: any) {
return firestore.collection('users').doc(uid).update(data);
}
export function createUser(uid: string, data: any) {
return firestore
.collection('users')
.doc(uid)
.set({ uid, ...data }, { merge: true });
}
现在我们可以轻松地使用这些库文件来构建钩子,以维护用户的会话和身份验证状态。
构建身份验证钩子。
我决定使用上下文 API 来处理身份验证状态。这样,我就可以轻松地在整个应用程序中访问任何身份验证变量。
首先,我创建了lib/auth.tsx。
然后我设置了钩子的上下文部分。
interface AuthContext {
auth: Auth | null;
loading: boolean;
signInWithTwitter: () => Promise<void>;
signOut: () => Promise<void>;
}
// Create context with a default state.
const authContext: Context<AuthContext> = createContext<AuthContext>({
auth: null,
loading: true,
signInWithTwitter: async () => {},
signOut: async () => {}
});
export function AuthProvider({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Helper to easily get auth context within components
export const useAuth = () => useContext(authContext);
现在到了更复杂的部分——实施useProvideAuth()。
function useProvideAuth() {
const [auth, setAuth] = useState<Auth | null>(null);
const [loading, setLoading] = useState<boolean>(true);
/**
* Callback function used for firebase.auth.onAuthStateChanged().
* Takes the user object returned and formats it for my state.
* We fetch the idToken and append it to my auth state and store it.
*/
const authStateChanged = async (authState: firebase.User | null) => {
// Formats response into my required state.
const formattedAuth = formatAuth(authState);
// Fetch firebase auth ID Token.
formattedAuth.token = await authState.getIdToken();
// Stores auth into state.
setAuth(formattedAuth);
// Sets loading state to false.
setLoading(false);
};
/**
* Callback function used for response from firebase OAuth.
* Store user object returned in firestore.
* @param firebase User Credential
*/
const signedIn = async (resp: firebase.auth.UserCredential) => {
// Format user into my required state.
const storeUser = formatAuth(resp.user);
// firestore database function
createUser(storeUser.uid, storeUser);
};
/**
* Callback for when firebase signOut.
* Sets auth state to null and loading to true.
*/
const clear = () => {
setAuth(null);
setLoading(true);
};
/**
* Triggers firebase Oauth for twitter and calls signIn when successful.
* sets loading to true.
*/
const signInWithTwitter = () => {
setLoading(true);
return firebase.auth().signInWithPopup(new firebase.auth.TwitterAuthProvider()).then(signedIn);
};
/**
* Calls firebase signOut and with clear callback to reset state.
*/
const signOut = () => {
return firebase.auth().signOut().then(clear);
};
/**
* Watches for state change for firebase auth and calls the handleUser callback
* on every change.
*/
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(authStateChanged);
return () => unsubscribe();
}, []);
// returns state values and callbacks for signIn and signOut.
return {
auth,
loading,
signInWithTwitter,
signOut
};
}
使用身份验证钩子
我添加AuthProvider到我的pages/_app.tsx。
import { AppProps } from 'next/app';
import { AuthProvider } from '../lib/auth';
import '../styles/globals.css';
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}
现在我们可以AuthContext在页面中使用它了。
我们可以添加一个登录按钮pages/index.tsx。如果用户通过身份验证,我们可以显示一个链接和一个注销按钮。
import { useAuth } from '../lib/auth';
import Link from 'next/link';
import { useEffect } from 'react';
export default function Home() {
const { auth, signOut, signInWithTwitter } = useAuth();
return (
<div>
{auth ? (
<div>
<Link href='/dashboard'>
<a>Dashboard</a>
</Link>
<button onClick={() => signOut()}>Sign Out</button>
</div>
) : (
<button onClick={() => signInWithTwitter()}>Sign In</button>
)}
</div>
);
}
我希望我的仪表盘路由受到身份验证保护。如果用户未通过身份验证,则会重定向回首页。
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useAuth } from '../lib/auth';
export default function Dashboard() {
const { auth, loading, signOut } = useAuth();
const router = useRouter();
useEffect(() => {
// If auth is null and we are no longer loading
if (!auth && !loading) {
// redirect to index
router.push('/');
}
}, [auth, loading]);
return (
<div>
<p>Dashboard: Hello World</p>
{auth && (
<div>
<button onClick={() => signOut()}>Sign Out</button>
</div>
)}
</div>
);
}
服务器端身份验证
服务器端身份验证将通过将客户端的 ID 令牌传递给 API 来实现。API 随后会在每个请求中验证该令牌。
我们首先创建一个获取工具来传递令牌util/fetcher.ts。
const fetcher = async (url: string, token: string) => {
const res = await fetch(url, {
method: 'GET',
headers: new Headers({ 'Content-Type': 'application/json', token }),
credentials: 'same-origin'
});
return res.json();
};
export default fetcher;
然后我们可以使用 API 路由验证令牌firebase-admin。
API路由:pages/api/user.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { auth } from '../../lib/firebase-admin';
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { uid } = await auth.verifyIdToken(req.headers.token);
res.status(200).json({ uid });
} catch (error) {
res.status(401).json({ error });
}
};
现在我们可以调用 API 来获取仪表盘页面中的用户数据。我使用useSWR钩子来处理 API 调用。
pages/dashboard.tsx
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import useSWR from 'swr';
import { useAuth } from '../lib/auth';
import fetcher from '../util/fetcher';
export default function Dashboard() {
const { auth, loading, signOut } = useAuth();
const router = useRouter();
useEffect(() => {
if (!auth && !loading) {
router.push('/');
}
}, [auth, loading]);
const { data } = useSWR(auth ? ['/api/user', auth.token] : null, fetcher);
return (
<div>
<p>Dashboard: Hello World</p>
{auth && (
<div>
<button onClick={() => signOut()}>Sign Out</button>
</div>
)}
{data && <div>{data}</div>}
</div>
);
}
结论
我现在已经使用 Firebase 为我的 Web 应用程序实现了身份验证。
- 用户可以使用 Twitter 的 OAuth 登录。
- 它会创建用户并将其存储在 Firebase 中。
- 我设置了一个受保护的路由,如果用户未通过身份验证,则会进行重定向。
- 我有一个受保护的端点,它会在每次请求时验证用户的令牌。
这是包含本文可用代码的仓库。
这或许不是最佳方案,但它能解决问题。
- Nest.js文档
- Firebase文档
- SWR文档
- 欢迎在Twitter上关注我,我会分享一些关于科技和编程的日常动态。我也会记录我的设计学习之旅。