跨Next.js Web、Kotlin移动端与NestJS+TypeORM API后端的健壮可扩展认证架构设计问询
跨Next.js Web、Kotlin移动端与NestJS+TypeORM API后端的健壮可扩展认证架构设计问询
我完全理解你现在的困境——当NextAuth默认的内置API路由模式遇到独立的NestJS后端时,确实会出现session和token协同的问题。针对你的多端认证需求,我整理了一套经过验证的架构方案和实践建议,希望能帮到你:
一、核心认证模式选型:JWT + Refresh Token(优先推荐)
对于多端(Web+Native)的无状态认证场景,JWT+Refresh Token模式是行业通用的最佳实践,原因如下:
- 无状态:NestJS后端不需要存储session,仅通过JWT的签名验证身份,适配高并发场景
- 跨平台:JWT是标准格式,Next.js、Kotlin移动端都能轻松解析和验证
- 灵活的有效期管理:短时效的Access Token(比如15分钟)降低泄露风险,长时效的Refresh Token(比如7天)避免用户频繁登录
- 可扩展:后续可以轻松接入OAuth2第三方登录(比如Google、微信),最终统一返回JWT+Refresh Token给客户端
如果你的业务需要支持第三方登录,也可以在NestJS侧集成OAuth2授权码模式,将第三方登录的结果转换为内部的JWT+Refresh Token,实现多端统一的认证体验。
二、中心化认证后端设计(NestJS + TypeORM)
基于NestJS的生态,你可以快速搭建统一的认证服务:
1. 依赖与核心组件
使用官方推荐的库:
@nestjs/jwt:生成和验证JWT@nestjs/passport+passport-jwt:实现JWT认证守卫bcrypt:加密用户密码
2. 核心端点设计
// auth.controller.ts import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { JwtService } from '@nestjs/jwt'; @Controller('auth') export class AuthController { constructor( private readonly authService: AuthService, private readonly jwtService: JwtService, ) {} // 登录接口:返回Access Token + Refresh Token @Post('login') async login(@Body() loginDto: { username: string; password: string }) { const user = await this.authService.validateUser(loginDto.username, loginDto.password); if (!user) throw new UnauthorizedException('Invalid credentials'); const payload = { sub: user.id, username: user.username }; // 生成Access Token(15分钟有效期) const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' }); // 生成Refresh Token(7天有效期),并存储到数据库 const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' }); await this.authService.saveRefreshToken(user.id, refreshToken); return { access_token: accessToken, refresh_token: refreshToken }; } // 刷新Access Token @Post('refresh') async refresh(@Body() refreshDto: { refresh_token: string }) { try { const payload = this.jwtService.verify(refreshDto.refresh_token); // 验证Refresh Token是否在数据库中有效 const isValid = await this.authService.validateRefreshToken(payload.sub, refreshDto.refresh_token); if (!isValid) throw new UnauthorizedException('Invalid refresh token'); // 生成新的双Token const newPayload = { sub: payload.sub, username: payload.username }; const newAccessToken = this.jwtService.sign(newPayload, { expiresIn: '15m' }); const newRefreshToken = this.jwtService.sign(newPayload, { expiresIn: '7d' }); await this.authService.updateRefreshToken(payload.sub, newRefreshToken); return { access_token: newAccessToken, refresh_token: newRefreshToken }; } catch (e) { throw new UnauthorizedException('Invalid refresh token'); } } // 登出:失效Refresh Token @Post('logout') async logout(@Body() logoutDto: { refresh_token: string }) { try { const payload = this.jwtService.verify(logoutDto.refresh_token); await this.authService.removeRefreshToken(payload.sub); return { message: 'Logged out successfully' }; } catch (e) { throw new UnauthorizedException('Invalid refresh token'); } } }
3. 数据库层设计(TypeORM)
创建RefreshToken实体,关联用户并存储有效Token:
// refresh-token.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { User } from '../users/user.entity'; @Entity() export class RefreshToken { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) token: string; @Column() userId: number; @Column({ type: 'timestamp' }) expiresAt: Date; @ManyToOne(() => User, user => user.refreshTokens) @JoinColumn({ name: 'userId' }) user: User; }
4. API路由保护
使用Passport JWT守卫保护需要认证的API:
// jwt-auth.guard.ts import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {} // 在控制器中使用 import { Controller, Get, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @Controller('users') @UseGuards(JwtAuthGuard) export class UsersController { @Get('me') getCurrentUser(@Request() req) { return req.user; // 从JWT解析出的用户信息 } }
三、Next.js前端集成方案
推荐结合NextAuth的CredentialsProvider来简化session管理,同时无缝对接NestJS的认证端点:
1. NextAuth配置(CredentialsProvider桥接)
// pages/api/auth/[...nextauth].ts import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; export const authOptions = { providers: [ CredentialsProvider({ name: "Credentials", credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.username || !credentials?.password) return null; // 调用NestJS登录接口 const res = await fetch(`${process.env.NESTJS_API_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: credentials.username, password: credentials.password, }), }); const user = await res.json(); if (res.ok && user) { // 返回包含Token的用户信息,存入NextAuth Session return { ...user, id: user.sub }; } return null; }, }), ], session: { strategy: "jwt" }, // 用JWT存储Session,避免NextAuth内置存储 callbacks: { async jwt({ token, user }) { // 登录时注入NestJS返回的Token if (user) { token.accessToken = user.access_token; token.refreshToken = user.refresh_token; token.accessTokenExpires = Date.now() + 15 * 60 * 1000; // 15分钟有效期 } // 自动刷新即将过期的Access Token if (Date.now() > token.accessTokenExpires - 60 * 1000) { try { const res = await fetch(`${process.env.NESTJS_API_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: token.refreshToken }), }); const newTokens = await res.json(); token.accessToken = newTokens.access_token; token.refreshToken = newTokens.refresh_token; token.accessTokenExpires = Date.now() + 15 * 60 * 1000; } catch (e) { console.error("Token refresh failed", e); } } return token; }, async session({ session, token }) { // 将Token注入Session,供页面/API调用使用 session.user.accessToken = token.accessToken; return session; }, }, }; export default NextAuth(authOptions);
2. 页面路由保护与API调用
// 受保护的页面组件 import { getServerSession } from "next-auth/next"; import { authOptions } from "../api/auth/[...nextauth]"; export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); if (!session) { return { redirect: { destination: "/login", permanent: false } }; } // 调用NestJS受保护API const res = await fetch(`${process.env.NESTJS_API_URL}/users/me`, { headers: { Authorization: `Bearer ${session.user.accessToken}` }, }); const userData = await res.json(); return { props: { userData } }; } export default function Profile({ userData }) { return <div>Welcome {userData.username}!</div>; }
四、Kotlin移动端集成方案
用OkHttp拦截器实现自动Token管理,结合加密存储保障安全:
1. 依赖配置
// app/build.gradle.kts dependencies { implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("androidx.security:security-crypto:1.1.0-alpha06") // 加密存储 implementation("org.json:json:20230227") }
2. Token管理与拦截器实现
// TokenManager.kt import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody class TokenManager(private val context: Context) { private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() private val encryptedPrefs = EncryptedSharedPreferences.create( context, "auth_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) private val client = OkHttpClient() fun saveTokens(accessToken: String, refreshToken: String) { encryptedPrefs.edit() .putString("access_token", accessToken) .putString("refresh_token", refreshToken) .apply() } fun getAccessToken(): String? = encryptedPrefs.getString("access_token", null) suspend fun refreshToken(): String? { val refreshToken = encryptedPrefs.getString("refresh_token", null) ?: return null val requestBody = """{"refresh_token":"$refreshToken"}""".toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url("${BuildConfig.NESTJS_API_URL}/auth/refresh") .post(requestBody) .build() return try { val response = client.newCall(request).execute() val responseBody = response.body?.string() ?: return null val json = org.json.JSONObject(responseBody) val newAccessToken = json.getString("access_token") val newRefreshToken = json.getString("refresh_token") saveTokens(newAccessToken, newRefreshToken) newAccessToken } catch (e: Exception) { e.printStackTrace() null } } } // AuthInterceptor.kt import okhttp3.Interceptor import okhttp3.Response class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val accessToken = tokenManager.getAccessToken() val authenticatedRequest = accessToken?.let { originalRequest.newBuilder() .addHeader("Authorization", "Bearer $it") .build() } ?: originalRequest val response = chain.proceed(authenticatedRequest) // 处理401错误,尝试刷新Token并重试 if (response.code == 401 && !originalRequest.url.pathSegments.contains("refresh")) { val newAccessToken = tokenManager.refreshToken() newAccessToken?.let { val newRequest = originalRequest.newBuilder() .addHeader("Authorization", "Bearer $it") .build() return chain.proceed(newRequest) } } return response } }
3. 初始化OkHttp客户端
// 在Application类中初始化 val tokenManager = TokenManager(applicationContext) val okHttpClient = OkHttpClient.Builder() .addInterceptor(AuthInterceptor(tokenManager)) .build()
五、关键最佳实践
Refresh Token安全:
- 每次刷新生成新的Refresh Token,旧Token立即从数据库删除
- 设置Refresh Token的过期时间,定期清理过期记录
- 禁止客户端重复使用同一个Refresh Token(数据库中标记使用状态)
Token存储安全:
- Web端:Refresh Token用
HttpOnly、Secure、SameSite=Strict的Cookie存储;Access Token用内存+短时效Cookie - 移动端:用
EncryptedSharedPreferences加密存储所有Token,禁止明文存储
- Web端:Refresh Token用
跨域配置:
- 在NestJS中配置CORS,允许Next.js域名并支持Cookie:
// main.ts app.enableCors({ origin: process.env.NEXTJS_URL, credentials: true, allowedHeaders: ['Content-Type', 'Authorization'], });
- 在NestJS中配置CORS,允许Next.js域名并支持Cookie:
监控与日志:
- 记录Token刷新、失效、异常登录事件,便于排查问题
- 监控401错误频率,识别异常请求行为
内容来源于stack exchange
相关产品推荐
相关产品推荐

