api-server/docs/ios登录流程-后端接口.md
WangDL fa69749884 refactor(auth): restructure auth system, align with iOS login flow spec
- Split AuthService into AppleAuthService, TokenService, AuthService
- Add dev-login endpoint (dev-only, disabled in production)
- AppleLoginDto: authorizationCode optional, add userIdentifier/email/fullName/nonce
- Login/refresh responses now include user object
- logout: single-token revoke + JwtAuthGuard protection
- users.repository: switch from in-memory Map to Prisma persistence
- JWT payload includes role, guards attach full user info to request
- Dual JWT secret support (JWT_ACCESS_SECRET / JWT_REFRESH_SECRET)
- Replace jwks-rsa+jsonwebtoken with jose library
- Prisma User model: add role field
- Independent DTO files with @Transform for empty string safety
- Add 5 iOS login flow documentation files
2026-05-13 17:31:50 +08:00

5.1 KiB
Raw Blame History

iOS 登录流程 —— 后端接口实现

本文覆盖后端的 dev-loginrefreshlogout/users/me 四个核心接口的实现要点。Apple 登录单独见 ios登录流程-Apple登录.md


一、dev-login 接口

开发阶段用的快速登录接口,生产环境必须禁止

请求

POST /api/auth/dev-login
{
  "email": "test@zhixi.app",
  "nickname": "测试用户",
  "devSecret": "你的开发密钥"
}

DTO

export class DevLoginDto {
  @IsEmail()
  email: string;

  @IsOptional()
  @IsString()
  nickname?: string;

  @IsString()
  @IsNotEmpty()
  devSecret: string;
}

后端逻辑

1. 判断 NODE_ENV 不是 production
2. 校验 devSecret
3. 根据 provider=DEV + providerUserId=email 查 AuthAccount
4. 如果没有,创建 User + AuthAccountprovider=DEV, providerUserId=email
5. 生成 accessToken
6. 生成 refreshToken
7. refreshToken hash 入库
8. 返回 token + user

生产环境保护

if (process.env.NODE_ENV === 'production') {
  throw new ForbiddenException('dev-login is disabled in production');
}

二、refresh 接口

用于 accessToken 过期后刷新登录态。

请求

POST /api/auth/refresh
{
  "refreshToken": "eyJ..."
}

后端逻辑

1. 校验 refreshToken JWT 签名
2. 解析出 userId / tokenId
3. 查 refresh_tokens 表,找到 tokenId 对应记录
4. 对比 tokenHashSHA-256
5. 确认 revokedAt 为 null未撤销
6. 确认 expiresAt 未过期
7. 生成新的 accessToken
8. 可选:轮换新的 refreshToken旧记录 revoke新记录入库
9. 返回新 token

响应(第一版可简单)

{
  "accessToken": "new_access_token",
  "refreshToken": "new_refresh_token"
}

建议做 refreshToken 轮换:每次都生成新的 refreshToken旧 token 标记 revoked这样即使 refreshToken 泄露也能被检测到。


三、logout 接口

请求

POST /api/auth/logout
Authorization: Bearer accessToken
{
  "refreshToken": "eyJ..."
}

后端逻辑

1. 通过 accessToken 拿到 currentUser
2. 解析 refreshToken拿到 tokenId
3. 查 refresh_tokens 表找到对应记录
4. 校验该记录属于当前用户userId 匹配)
5. 设置 revokedAt = now()
6. 返回成功

iOS 侧配合操作

清除 Keychain 中的 refreshToken
清除内存中的 accessToken + user
跳转到登录页

四、/users/me 接口

App 启动后判断登录态的核心接口。

请求

GET /api/users/me
Authorization: Bearer accessToken

响应

{
  "id": "user_xxx",
  "email": "test@zhixi.app",
  "nickname": "测试用户",
  "avatarUrl": null,
  "role": "USER",
  "status": "ACTIVE",
  "onboardingCompleted": false
}

后端逻辑

1. JwtAuthGuard 校验 accessToken
2. 从 JWT payload 取 currentUser.id
3. 查 users 表返回用户信息

注意不要返回敏感字段如密码哈希、token 等),只返回前端需要的用户展示信息。


五、JwtAuthGuard

全局认证守卫,保护需要登录的接口。

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

配合 jwt.strategy.ts 从 Authorization Header 解析 JWT注入 currentUser

CurrentUser 装饰器

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: keyof User | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);

使用示例

@Get('knowledge-bases')
@UseGuards(JwtAuthGuard)
async list(@CurrentUser('id') userId: string) {
  return this.service.findByUser(userId);
}

六、通用 Provider 登录方法

auth.service.ts 中的通用方法,被 dev-login 和 Apple 登录复用:

async loginWithProvider(params: {
  provider: AuthProvider;
  providerUserId: string;
  email?: string;
  nickname?: string;
}) {
  // 1. 查 auth_account
  let authAccount = await this.prisma.authAccount.findUnique({
    where: {
      provider_providerUserId: {
        provider: params.provider,
        providerUserId: params.providerUserId,
      },
    },
    include: { user: true },
  });

  // 2. 没有就创建
  if (!authAccount) {
    const user = await this.prisma.user.create({
      data: {
        email: params.email,
        nickname: params.nickname,
        authAccounts: {
          create: {
            provider: params.provider,
            providerUserId: params.providerUserId,
            email: params.email,
          },
        },
      },
    });
    authAccount = { user, /* ... */ };
  }

  // 3. 签发 token
  const accessToken = this.tokenService.generateAccessToken(authAccount.user);
  const refreshToken = this.tokenService.generateRefreshToken(authAccount.user);

  // 4. refreshToken hash 入库
  await this.tokenService.saveRefreshToken(authAccount.user.id, refreshToken);

  // 5. 返回
  return { accessToken, refreshToken, user: authAccount.user };
}