在 Node.js + TypeScript 应用中集成 Google OAuth 2.0 与 JWT 中使用 Passport.js

让我们直奔主题——**身份验证** 是一件痛苦的事情。如果您曾经使用过 `Passport.js`,您就会知道它不仅仅是一件痛苦的事情;它还是一件非常令人头痛的事情。它功能强大,是的,但也非常抽象,有时甚至过于复杂。但问题是——如果您想要一个可靠的身份验证流程,那么它值得您付出努力。将它与 **Google OAuth 2.0** 和 **JWT** 配对,您就会获得一个安全且可扩展的解决方案。在本文中,我将向您展示如何使用 **TypeScript**、**Passport** 和 **JWT** 将 **Google OAuth 2.0** 集成到 **Node.js** 应用程序中。最后,您将拥有一个尽可能好的身份验证设置,而不会出现通常的“它只在本地主机上有效”的问题。让我们开始吧。

在深入实施之前,让我先澄清一件事——我会尽可能地保持 **真实**。我们不会只是拼凑一个“有点真实”的服务器;我们会构建一个坚实的东西,并融入 **最佳实践**。这里的目标不仅仅是向您展示如何做,而且我们正在做。让我们把这件事做好。

项目结构

这是文件结构,只是为了让您了解它的样子:

google-oauth-jwt-node/  
├── src/  
│   ├── database/  
│   │   ├── models/  
│   │   │   └── User.ts  
│   ├── middlewares/  
│   │   └── requireJwt.ts  
│   ├── auth/  
│   │   ├── google.ts  
│   │   ├── jwt.ts  
│   │   └── passport.ts  
│   ├── routes/  
│   │   └── authRoute.ts  
│   │   └── userRoute.ts  
│   └── app.ts  
├── .env  
├── package.json  
├── tsconfig.json  
├── .gitignore

让我们初始化项目

首先,让我们创建并初始化 Node.js 项目:

# you may use any name you want
mkdir google-oauth-jwt-node
cd google-oauth-jwt-node

npm init -y

其次,我们将安装我们将要使用的依赖项:

  • express — Node.js 的 Web 框架。
  • 护照——Node.js 的身份验证中间件。
  • Passport-google-oauth20 — Google OAuth 2.0 身份验证的 Passport 策略。
  • Passport-jwt — 用于 JWT 身份验证的 Passport 策略,它将负责验证 JWT 令牌。
  • jsonwebtoken —生成并验证 JWT 令牌以进行身份​​验证。
  • bcrypt — 用于散列和比较密码的库。
  • uuid — 用于生成唯一标识符(我们将使用它来获取安全的 JWT 令牌)。
  • dotenv —从 .env 文件加载环境变量。
  • ts-node — 用于直接运行 TypeScript 文件的 TypeScript 执行环境。
  • nodemon —当文件发生变化时自动重启服务器的开发工具。
  • 不要忘记安装@types/包作为需要它们的开发依赖项。

    要安装这些,只需运行:

    # dependencies
    npm install express passport passport-google-oauth20 passport-jwt jsonwebtoken bcrypt uuid dotenv
    
    # devDependencies
    npm install @types/node @types/express @types/passport @types/passport-google-oauth20 @types/passport-jwt @types/jsonwebtoken @types/bcrypt --save-dev
    
    npm install nodemon typescript ts-node --save-dev

    接下来,我们需要初始化 TypeScript:

    npx tsc --init

    这是一个简单的 `tsconfig.json` 配置:

    {
      "compilerOptions": {
        "target": "ES6",
        "module": "CommonJS",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "outDir": "./dist",
        "baseUrl": "./src"
      },
      "include": ["src/**/*"],
      "exclude": ["node_modules", "dist"]
    }

    我们不要忘记创建 `.gitignore`:

    node_modules/
    .env
    dist/

    将这些脚本添加到你的“package.json”中:

    "scripts": {
      "dev": "nodemon src/app.ts",        // start in development with automatic restarts
      "build": "tsc"                      // to compile TypeScript
    }

    另外,将这些放入你的 `.env` 文件中:

    GOOGLE_CLIENT_ID=your-google-client-id
    GOOGLE_CLIENT_SECRET=your-google-client-secret
    JWT_SECRET=your-jwt-secret
    # you can change these with whatever you want
    BE_BASE_URL=http://localhost:3000
    FE_BASE_URL=http://localhost:3001

    获取 Google OAuth 凭证(客户端 ID 和密钥)是您的责任。这是一个简单的过程,但如果您不熟悉它,可能会不知所措。网上有很多指南,所以我不会在这里介绍它——我指望你能弄清楚。

    只是不要忘记添加 `http://localhost:3000` 作为**授权 JavaScript 来源**,并添加 `http://localhost:3000/api/auth/google/callback` 作为**授权重定向 URI**。

    现在我们开始实施该项目

    这是我们的`app.ts`:

    // app.ts
    import express, { Request, Response } from 'express';
    import passport from './auth/passport';  // passport configuration
    import dotenv from 'dotenv';
    import { json } from 'body-parser';
    import authRoute from './routes/authRoute';  // our authRoute
    import userRoute from './routes/userRoute';  // our userRoute
    
    // to load environment variables from .env file
    dotenv.config();
    
    const app = express();
    
    // middleware to parse json bodies
    app.use(json());
    
    // authRoute
    app.use('/api/auth', authRoute);
    
    // userRoute
    app.use('/api/user', userRoute);
    
    // default route
    app.get('/', (req: Request, res: Response) => {
      res.send('welcome to the Google OAuth 2.0 + JWT Node.js app!');
    });
    
    // start the server
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
      console.log(`server is running on http://localhost:${PORT}`);
    });

    然后,我们开始使用 Passport 实现 **身份验证策略**。这些策略包括使用 **Google OAuth 2.0**(用于使用 Google 帐户对用户进行身份验证)和使用 JWT(用于使用基于令牌的身份验证保护路由)。

    `src/auth/passport.ts`:

    // passport.ts
    import passport from 'passport';
    import { googleStrategy } from './google';
    import { jwtStrategy } from './jwt';
    
    // initialize passport with Google and JWT strategies
    passport.use('google', googleStrategy);
    passport.use('jwtAuth', jwtStrategy);
    
    export default passport;

    `src/auth/google.ts`:

    // google.ts
    import { Strategy as GoogleStrategy, Profile, VerifyCallback } from 'passport-google-oauth20';
    import User from '../database/models/User'; // mock user class
    import { v4 as uuidv4 } from 'uuid';
    
    const options = {
      clientID: process.env.GOOGLE_CLIENT_ID || '',
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
      callbackURL: `${process.env.BE_BASE_URL}/api/auth/google/callback`,
    };
    
    async function verify(accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback) {
      try {
        // we check for if the user is present in our system/database.
        // which states that; is that a sign-up or sign-in?
        let user = await User.findOne({
          where: {
            googleId: profile.id,
          },
        });
    
        // if not
        if (!user) {
          // create new user if doesn't exist
          user = await User.create({
            googleId: profile.id,
            email: profile.emails?.[0]?.value,
            fullName: profile.displayName,
            jwtSecureCode: uuidv4(),
          });
        }
    
        // auth the User
        return done(null, user);
      } catch (error) {
        return done(error as Error);
      }
    }
    
    export default new GoogleStrategy(options, verify);

    `src/auth/jwt.ts`:

    // jwt.ts
    import { Strategy, ExtractJwt, VerifiedCallback } from 'passport-jwt';
    import User from '../database/models/User'; // mock user class
    import bcrypt from 'bcrypt';
    
    const options = {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || 'secret-test',
    };
    
    async function verify(payload: any, done: VerifiedCallback) {
      /* 
        a valid JWT in our system must have `id` and `jwtSecureCode`.
        you can create your JWT like the way you like.
      */
      // bad path: JWT is not valid
      if (!payload?.id || !payload?.jwtSecureCode) {
        return done(null, false);
      }
    
      // try to find a User with the `id` in the JWT payload.
      const user = await User.findOne({
        where: {
          id: payload.id,
        },
      });
    
      // bad path: User is not found.
      if (!user) {
        return done(null, false);
      }
    
      // compare User's jwtSecureCode with the JWT's `jwtSecureCode` that the 
      // request has.
      // bad path: bad JWT, it sucks.
      if (!bcrypt.compareSync(user.jwtSecureCode, payload.jwtSecureCode)) {
        return done(null, false);
      }
    
      // happy path: JWT is valid, we auth the User.
      return done(null, user);
    }
    
    export default new Strategy(options, verify);

    最后,我们在 `passport.ts` 中设置了 **Google OAuth2.0** 和 **JWT 身份验证** 所需的策略,并指示 **Passport.js** 使用它们。

    现在,让我们通过实现 `requireJwt.ts` **中间件**来保护我们的**路由**,这将确保只有使用 **有效的 accessToken** 才能访问 **受保护的路由**。

    `src/middlewares/requireJwt.ts`:

    // requireJwt.ts
    import passport from '../auth/passport';  // import passport from our custom passport file
    
    // requireJwt middleware to authenticate the request using JWT
    const requireJwt = passport.authenticate('jwtAuth', { session: false });
    
    export default requireJwt;

    到目前为止,我们已经设置了项目,使 **Google OAuth2.0** 和 **JWT** 与 **Passport.js** 协同工作,并添加了 **中间件** 以保护需要访问令牌的路由。现在,我们将在 `authRoute.ts` 中实现必要的端点,以处理应用程序的 **Google 登录** 和 **注册**。我们还将创建一个模拟端点 `GET /api/user/` 来返回用户信息,该信息只能通过有效的访问令牌访问,因为我们将在路由中添加 `requireJwt.ts` 中间件。

    `src/routes/authRoute.ts`:

    // authRoute.ts
    import { Request, Response, Router } from 'express';
    import passport from '../auth/passport';  // import passport from our custom passport file
    import * as AuthService from '../services/AuthService';  // assuming you have a service
    
    const router = Router();
    
    /*
      This route triggers the Google sign-in/sign-up flow. 
      When the frontend calls it, the user will be redirected to the 
      Google accounts page to log in with their Google account.
    */
    // Google OAuth2.0 route
    router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
    
    
    /*
      This route is the callback endpoint for Google OAuth2.0. 
      After the user logs in via Google's authentication flow, they are redirected here.
      Passport.js processes the callback, attaches the user to req.user, and we handle 
      the access token generation and redirect the user to the frontend.
    */
    // Google OAuth2.0 callback route
    router.get('/google/callback', passport.authenticate('google', { session: false }), (req: Request, res: Response) => {
      try {
        // we can use req.user because the GoogleStrategy that we've 
        // implemented in `google.ts` attaches the user
        const user = req.user as User;
    
        // handle the google callback, generate auth token
        const { authToken } = AuthService.handleGoogleCallback({ id: user.id, jwtSecureCode: user.jwtSecureCode });
    
        // redirect to frontend with the accessToken as query param
        const redirectUrl = `${process.env.FE_BASE_URL}?accessToken=${authToken}`;
        return res.redirect(redirectUrl);
      } catch (error) {
        return res.status(500).json({ message: 'An error occurred during authentication', error });
      }
    });
    
    export default router;

    `src/routes/userRoute.ts`:

    // userRoute.ts
    import { Request, Response, Router } from 'express';
    import requireJwt from '../middlewares/requireJwt'; // our middleware to authenticate using JWT
    import UserService from '../services/UserService' // assuming you have a service
    
    const router = Router();
    
    // mock user info endpoint to return user data
    router.get('/', requireJwt, (req: Request, res: Response) => {
      try {
        /* 
           The requireJwt middleware authenticates the request by verifying 
           the accessToken. Once authenticated, it attaches the User object 
           to req.user (see `jwt.ts`), making it availabe in the subsequent route handlers, 
           like those in userRoute.
        */
        // req.user is populated after passing through the requireJwt 
        // middleware
        const user = req.user as User;
    
        const veryVerySecretUserInfo = await UserService.getUser({ userId: user.id });
    
        // it is a mock, you MUST return only the necessary info :)
        return res.status(200).json(veryVerySecretUserInfo);
      } catch (error) {
        return res.status(500).json({ message: 'An error occurred while fetching user info', error });
      }
    });
    
    export default router;

    但是...法提赫...我该如何测试它?

    我听到你说:“别担心,我懂你。以下是本地测试所有内容的方法:启动你的服务器,然后按照我的指示操作。

    我们首先需要**运行**服务器,而不是真正启动它...我们不是纵火犯,对吧?

    只需输入:

    npm run dev

    你会看到输出 `server is running on http://localhost:3000`

    现在,我们只需打开浏览器并粘贴此内容:

    localhost:3000/api/auth/google

    如果您所有操作都正确,您将看到此屏幕;

    google_signin_screen

    这基本上就是当有人点击你前端的**使用 Google 注册**按钮时发生的情况。

    当您从该列表中选择一个帐户时,您将被重定向到`localhost:3001/?accessToken=${accessToken}`。然后 bada bing bada boom,您的**前端**现在需要处理此`accessToken`,将其保存在`localStorage`中,并向`GET api/user/`端点发送请求以获取必要的用户信息。之后,您可以授权用户。

    结论

    就是这样!您已成功设置了 Google 登录和 JWT,现在您可以在应用中处理用户身份验证和授权。您已了解了如何使用 Google OAuth2.0 设置 Passport、使用 JWT 进行会话管理以及使用简单的中间件保护路由。

    接下来,只需连接前端和后端,正确处理令牌,并为用户提供流畅的体验。您已准备好继续构建,所以继续测试吧!

    thank_you