# iOS 登录流程 —— 后端接口实现 本文覆盖后端的 `dev-login`、`refresh`、`logout`、`/users/me` 四个核心接口的实现要点。Apple 登录单独见 [ios登录流程-Apple登录.md](./ios登录流程-Apple登录.md)。 --- ## 一、dev-login 接口 开发阶段用的快速登录接口,**生产环境必须禁止**。 ### 请求 ```http POST /api/auth/dev-login ``` ```json { "email": "test@zhixi.app", "nickname": "测试用户", "devSecret": "你的开发密钥" } ``` ### DTO ```ts 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 + AuthAccount(provider=DEV, providerUserId=email) 5. 生成 accessToken 6. 生成 refreshToken 7. refreshToken hash 入库 8. 返回 token + user ``` ### 生产环境保护 ```ts if (process.env.NODE_ENV === 'production') { throw new ForbiddenException('dev-login is disabled in production'); } ``` --- ## 二、refresh 接口 用于 accessToken 过期后刷新登录态。 ### 请求 ```http POST /api/auth/refresh ``` ```json { "refreshToken": "eyJ..." } ``` ### 后端逻辑 ``` 1. 校验 refreshToken JWT 签名 2. 解析出 userId / tokenId 3. 查 refresh_tokens 表,找到 tokenId 对应记录 4. 对比 tokenHash(SHA-256) 5. 确认 revokedAt 为 null(未撤销) 6. 确认 expiresAt 未过期 7. 生成新的 accessToken 8. 可选:轮换新的 refreshToken(旧记录 revoke,新记录入库) 9. 返回新 token ``` ### 响应(第一版可简单) ```json { "accessToken": "new_access_token", "refreshToken": "new_refresh_token" } ``` **建议做 refreshToken 轮换**:每次都生成新的 refreshToken,旧 token 标记 revoked,这样即使 refreshToken 泄露也能被检测到。 --- ## 三、logout 接口 ### 请求 ```http POST /api/auth/logout Authorization: Bearer accessToken ``` ```json { "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 启动后判断登录态的核心接口。 ### 请求 ```http GET /api/users/me Authorization: Bearer accessToken ``` ### 响应 ```json { "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 全局认证守卫,保护需要登录的接口。 ```ts @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {} ``` 配合 `jwt.strategy.ts` 从 Authorization Header 解析 JWT,注入 `currentUser`。 ### CurrentUser 装饰器 ```ts 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; }, ); ``` ### 使用示例 ```ts @Get('knowledge-bases') @UseGuards(JwtAuthGuard) async list(@CurrentUser('id') userId: string) { return this.service.findByUser(userId); } ``` --- ## 六、通用 Provider 登录方法 `auth.service.ts` 中的通用方法,被 dev-login 和 Apple 登录复用: ```ts 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 }; } ```