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

269 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 + AuthAccountprovider=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. 对比 tokenHashSHA-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 };
}
```