You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

跨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()

五、关键最佳实践

  1. Refresh Token安全:

    • 每次刷新生成新的Refresh Token,旧Token立即从数据库删除
    • 设置Refresh Token的过期时间,定期清理过期记录
    • 禁止客户端重复使用同一个Refresh Token(数据库中标记使用状态)
  2. Token存储安全:

    • Web端:Refresh Token用HttpOnly、Secure、SameSite=Strict的Cookie存储;Access Token用内存+短时效Cookie
    • 移动端:用EncryptedSharedPreferences加密存储所有Token,禁止明文存储
  3. 跨域配置:

    • 在NestJS中配置CORS,允许Next.js域名并支持Cookie:
      // main.ts
      app.enableCors({
        origin: process.env.NEXTJS_URL,
        credentials: true,
        allowedHeaders: ['Content-Type', 'Authorization'],
      });
      
  4. 监控与日志:

    • 记录Token刷新、失效、异常登录事件,便于排查问题
    • 监控401错误频率,识别异常请求行为

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 14:25:32