在Next.js与NestJS中集成Google OAuth 2.0的实现方案问询
Next.js + NestJS 实现 Google OAuth 2.0 完整指南
一、整体交互流程
- 用户在Next.js客户端点击Google登录按钮,客户端重定向到Google OAuth授权页面
- 用户完成Google账号验证并授权后,Google将授权码发送到NestJS后端的回调地址
- 后端用授权码向Google请求获取访问令牌(access_token)和用户信息
- 后端验证用户信息后,生成自定义的JWT访问令牌和刷新令牌,将刷新令牌存入HttpOnly Cookie,返回访问令牌给客户端
- 客户端存储访问令牌到内存,后续发起API请求时携带该令牌
- 访问令牌过期后,客户端用Cookie中的刷新令牌请求后端获取新的访问令牌
二、Next.js 客户端实现
1. 环境配置
在项目根目录创建.env.local,添加Google OAuth配置和NextAuth密钥:
GOOGLE_CLIENT_ID=你的Google客户端ID GOOGLE_CLIENT_SECRET=你的Google客户端密钥 NEXTAUTH_SECRET=随机生成的密钥(可用`openssl rand -hex 32`生成) NEXT_PUBLIC_API_URL=http://localhost:3001(你的NestJS后端地址)
2. 集成NextAuth.js
创建app/api/auth/[...nextauth]/route.ts配置认证路由:
import NextAuth from "next-auth"; import GoogleProvider from "next-auth/providers/google"; export const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, authorization: { params: { scope: "openid email profile" } }, }), ], session: { strategy: "jwt" }, callbacks: { async jwt({ token, account }) { if (account) { token.accessToken = account.access_token; token.refreshToken = account.refresh_token; token.expiresAt = account.expires_at; } return token; }, async session({ session, token }) { session.user.accessToken = token.accessToken as string; session.user.refreshToken = token.refreshToken as string; session.user.expiresAt = token.expiresAt as number; return session; }, }, secret: process.env.NEXTAUTH_SECRET!, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
3. 登录状态管理与UI
创建登录按钮组件app/components/LoginButton.tsx:
"use client"; import { useSession, signIn, signOut } from "next-auth/react"; export default function LoginButton() { const { data: session } = useSession(); if (session) { return ( <div> <p>欢迎, {session.user.name}</p> <button onClick={() => signOut()}>退出登录</button> </div> ); } return <button onClick={() => signIn("google")}>使用Google登录</button>; }
在页面中使用该组件,通过useSession钩子判断登录状态,渲染受保护内容。
4. API请求拦截器
创建app/lib/axios.ts配置请求拦截,自动携带令牌并处理过期刷新:
import axios from "axios"; import { getSession, updateSession } from "next-auth/react"; const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, withCredentials: true, }); api.interceptors.request.use(async (config) => { const session = await getSession(); if (session?.user.accessToken) { config.headers.Authorization = `Bearer ${session.user.accessToken}`; } return config; }); api.interceptors.response.use( (res) => res, async (err) => { const originalRequest = err.config; if (err.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; const session = await getSession(); if (session?.user.refreshToken) { try { const refreshRes = await api.post("/auth/refresh"); const newAccessToken = refreshRes.data.accessToken; // 更新session中的令牌 await updateSession({ ...session, user: { ...session.user, accessToken: newAccessToken }, }); originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return api(originalRequest); } catch (refreshErr) { // 刷新失败,清除session并跳转登录 await signOut(); return Promise.reject(refreshErr); } } } return Promise.reject(err); } ); export default api;
三、NestJS 后端实现
1. 安装依赖
npm install @nestjs/passport passport-google-oauth20 @nestjs/jwt passport-jwt cookie-parser csurf npm install -D @types/passport-google-oauth20 @types/passport-jwt @types/cookie-parser
2. 配置环境变量
创建.env文件:
GOOGLE_CLIENT_ID=你的Google客户端ID GOOGLE_CLIENT_SECRET=你的Google客户端密钥 GOOGLE_CALLBACK_URL=http://localhost:3001/auth/google/callback JWT_SECRET=随机生成的JWT密钥 PORT=3001 FRONTEND_URL=http://localhost:3000(你的Next.js前端地址)
3. 配置Google OAuth策略
创建src/auth/google.strategy.ts:
import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Strategy, VerifyCallback } from "passport-google-oauth20"; import { ConfigService } from "@nestjs/config"; @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy, "google") { constructor(private configService: ConfigService) { super({ clientID: configService.get("GOOGLE_CLIENT_ID"), clientSecret: configService.get("GOOGLE_CLIENT_SECRET"), callbackURL: configService.get("GOOGLE_CALLBACK_URL"), scope: ["email", "profile"], }); } async validate( accessToken: string, refreshToken: string, profile: any, done: VerifyCallback ) { const user = { email: profile.emails[0].value, firstName: profile.name.givenName, lastName: profile.name.familyName, avatar: profile.photos[0].value, googleId: profile.id, }; done(null, user); } }
4. JWT认证配置
创建src/auth/jwt.strategy.ts:
import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { ExtractJwt, Strategy } from "passport-jwt"; import { ConfigService } from "@nestjs/config"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private configService: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get("JWT_SECRET"), }); } async validate(payload: any) { return { userId: payload.sub, email: payload.email }; } }
创建src/auth/jwt-auth.guard.ts:
import { Injectable } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; @Injectable() export class JwtAuthGuard extends AuthGuard("jwt") {}
5. 认证服务与控制器
创建src/auth/auth.service.ts:
import { Injectable } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import { ConfigService } from "@nestjs/config"; @Injectable() export class AuthService { constructor( private jwtService: JwtService, private configService: ConfigService ) {} async generateTokens(user: any) { const payload = { sub: user.email, email: user.email }; const accessToken = this.jwtService.sign(payload, { expiresIn: "15m" }); const refreshToken = this.jwtService.sign(payload, { expiresIn: "7d" }); return { accessToken, refreshToken }; } async validateRefreshToken(token: string) { try { return this.jwtService.verify(token); } catch (err) { return null; } } }
创建src/auth/auth.controller.ts:
import { Controller, Get, UseGuards, Res, Req, Post } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { AuthService } from "./auth.service"; import { Response, Request } from "express"; import { ConfigService } from "@nestjs/config"; @Controller("auth") export class AuthController { constructor( private authService: AuthService, private configService: ConfigService ) {} @Get("google") @UseGuards(AuthGuard("google")) async googleAuth() {} @Get("google/callback") @UseGuards(AuthGuard("google")) async googleAuthRedirect(@Req() req, @Res() res: Response) { const { accessToken, refreshToken } = await this.authService.generateTokens( req.user ); // 设置HttpOnly Cookie存储Refresh Token res.cookie("refresh_token", refreshToken, { httpOnly: true, secure: this.configService.get("NODE_ENV") === "production", sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, // 7天 }); // 重定向回客户端,并携带accessToken res.redirect( `${this.configService.get("FRONTEND_URL")}?accessToken=${accessToken}` ); } @Post("refresh") async refreshToken(@Req() req: Request, @Res() res: Response) { const refreshToken = req.cookies["refresh_token"]; if (!refreshToken) return res.status(401).send("Refresh token missing"); const payload = await this.authService.validateRefreshToken(refreshToken); if (!payload) return res.status(401).send("Invalid refresh token"); const newAccessToken = this.jwtService.sign( { sub: payload.sub, email: payload.email }, { expiresIn: "15m" } ); res.send({ accessToken: newAccessToken }); } }
6. 配置Auth模块
修改src/auth/auth.module.ts:
import { Module } from "@nestjs/common"; import { PassportModule } from "@nestjs/passport"; import { JwtModule } from "@nestjs/jwt"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { GoogleStrategy } from "./google.strategy"; import { JwtStrategy } from "./jwt.strategy"; import { AuthService } from "./auth.service"; import { AuthController } from "./auth.controller"; @Module({ imports: [ PassportModule, JwtModule.registerAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ secret: configService.get("JWT_SECRET"), }), inject: [ConfigService], }), ConfigModule, ], providers: [GoogleStrategy, JwtStrategy, AuthService], controllers: [AuthController], exports: [AuthService], }) export class AuthModule {}
7. 保护API端点
在需要保护的控制器中使用JwtAuthGuard:
import { Controller, Get, UseGuards } from "@nestjs/common"; import { JwtAuthGuard } from "../auth/jwt-auth.guard"; @Controller("users") export class UsersController { @Get("profile") @UseGuards(JwtAuthGuard) getProfile() { return { message: "受保护的用户资料", data: { email: "user@example.com" } }; } }
8. 启用Cookie解析与CSRF防护
修改src/main.ts:
import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; import * as cookieParser from "cookie-parser"; import * as csurf from "csurf"; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: process.env.FRONTEND_URL, credentials: true, }); app.use(cookieParser()); // 启用CSRF防护,从Cookie获取令牌 app.use( csurf({ cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", }, }) ); // 暴露CSRF令牌给客户端 app.use((req, res, next) => { res.cookie("XSRF-TOKEN", req.csrfToken()); next(); }); await app.listen(process.env.PORT || 3001); } bootstrap();
四、令牌管理细节
1. 令牌生成
- Access Token: 由NestJS签发,有效期15分钟,存储在客户端内存中,用于API请求认证
- Refresh Token: 由NestJS签发,有效期7天,存储在HttpOnly Cookie中,用于刷新Access Token
2. 令牌验证
- 后端通过
JwtStrategy验证Access Token的签名和有效期,验证通过后才允许访问受保护API - 刷新令牌时,后端验证Refresh Token的有效性,验证通过后签发新的Access Token
3. 令牌续订
- 客户端通过Axios拦截器检测401响应,自动触发刷新令牌流程
- 刷新成功后更新内存中的Access Token,继续原请求;失败则清除登录状态,跳转登录页面
五、安全策略
- 强制HTTPS: 生产环境必须启用HTTPS,防止令牌被窃听
- 令牌存储:
- Access Token仅存于内存,避免XSS攻击窃取
- Refresh Token存储在HttpOnly、Secure、SameSite=Strict的Cookie中,防止JS读取和CSRF攻击
- CSRF防护: 启用CSRF中间件,客户端请求时携带XSRF-Token
- 输入验证: 所有API请求参数使用
class-validator进行验证,防止注入攻击 - 环境变量: 敏感配置(如密钥、客户端ID)存储在环境变量中,不硬编码到代码
六、交互流程图
sequenceDiagram participant 用户 participant Next.js客户端 participant NestJS后端 participant Google OAuth服务 用户->>Next.js客户端: 点击Google登录按钮 Next.js客户端->>Google OAuth服务: 重定向到授权页面(带client_id、scope) Google OAuth服务->>用户: 展示登录授权界面 用户->>Google OAuth服务: 确认授权 Google OAuth服务->>NestJS后端: 重定向到回调地址,携带授权code NestJS后端->>Google OAuth服务: 用code、client_id、client_secret请求令牌 Google OAuth服务->>NestJS后端: 返回access_token、id_token、用户信息 NestJS后端->>NestJS后端: 验证用户信息,生成自定义JWT令牌 NestJS后端->>Next.js客户端: 设置HttpOnly Cookie存refresh_token,返回access_token Next.js客户端->>Next.js客户端: 存储access_token到内存,更新登录状态 用户->>Next.js客户端: 访问受保护页面/发起API请求 Next.js客户端->>NestJS后端: 携带Authorization: Bearer <access_token>请求API NestJS后端->>NestJS后端: 验证access_token有效性 NestJS后端->>Next.js客户端: 返回API数据 Note over Next.js客户端,NestJS后端: Access Token过期时 Next.js客户端->>NestJS后端: 携带Cookie中的refresh_token请求刷新 NestJS后端->>NestJS后端: 验证refresh_token有效性 NestJS后端->>Next.js客户端: 返回新的access_token Next.js客户端->>Next.js客户端: 更新内存中的access_token,继续请求
内容的提问来源于stack exchange,提问作者Romel Gomez
相关产品推荐
相关产品推荐

