269 lines
5.1 KiB
Markdown
269 lines
5.1 KiB
Markdown
# 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 };
|
||
}
|
||
```
|