input('username'); $password = $request->input('password'); $email = $request->input('email'); // 校验 username if (!is_string($username) || trim($username) === '') { return $response->json([ 'code' => 400, 'message' => '用户名不能为空', ])->withStatus(400); } $username = trim($username); $username_length = strlen($username); if ($username_length < 3 || $username_length > 20) { return $response->json([ 'code' => 400, 'message' => '用户名长度需在 3-20 个字符', ])->withStatus(400); } // 校验 password if (!is_string($password) || $password === '') { return $response->json([ 'code' => 400, 'message' => '密码不能为空', ])->withStatus(400); } $password_length = strlen($password); if ($password_length < 6 || $password_length > 32) { return $response->json([ 'code' => 400, 'message' => '密码长度需在 6-32 个字符', ])->withStatus(400); } // 校验 email if (!is_string($email) || trim($email) === '') { return $response->json([ 'code' => 400, 'message' => '邮箱不能为空', ])->withStatus(400); } $email = trim($email); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return $response->json([ 'code' => 400, 'message' => '邮箱格式不正确', ])->withStatus(400); } if (strlen($email) > 100) { return $response->json([ 'code' => 400, 'message' => '邮箱长度不能超过 100 个字符', ])->withStatus(400); } // 唯一性检查 if (User::query()->where('username', $username)->exists()) { return $response->json([ 'code' => 400, 'message' => '用户名已存在', ])->withStatus(400); } if (User::query()->where('email', $email)->exists()) { return $response->json([ 'code' => 400, 'message' => '邮箱已被注册', ])->withStatus(400); } // 创建用户 $user = User::create([ 'username' => $username, 'password' => $password, 'email' => $email, 'status' => 1, ]); return [ 'code' => 0, 'message' => '注册成功', 'data' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, ], ]; } /** * 用户登录 */ #[OA\Post( path: '/login', summary: '用户登录', description: '使用用户名和密码登录,返回 access_token 和 refresh_token', tags: ['Auth'], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['username', 'password'], properties: [ new OA\Property(property: 'username', type: 'string', example: 'admin'), new OA\Property(property: 'password', type: 'string', example: 'Pass_1234'), ] ) ), responses: [ new OA\Response( response: 200, description: '登录成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: '登录成功'), new OA\Property(property: 'data', properties: [ new OA\Property(property: 'access_token', type: 'string'), new OA\Property(property: 'refresh_token', type: 'string'), new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'), new OA\Property(property: 'expires_in', type: 'integer', example: 7200), new OA\Property(property: 'user', properties: [ new OA\Property(property: 'id', type: 'integer'), new OA\Property(property: 'username', type: 'string'), new OA\Property(property: 'email', type: 'string'), ], type: 'object'), ], type: 'object'), ]) ), new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 401, description: '用户名或密码错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "login", methods: "POST")] public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $username = $request->input('username'); $password = $request->input('password'); // 校验参数 if (!is_string($username) || trim($username) === '') { return $response->json([ 'code' => 400, 'message' => '用户名不能为空', ])->withStatus(400); } if (!is_string($password) || $password === '') { return $response->json([ 'code' => 400, 'message' => '密码不能为空', ])->withStatus(400); } // 查找用户 $user = User::query()->where('username', trim($username))->first(); if (!$user) { return $response->json([ 'code' => 401, 'message' => '用户名或密码错误', ])->withStatus(401); } // 验证密码 if (!$user->verifyPassword($password)) { return $response->json([ 'code' => 401, 'message' => '用户名或密码错误', ])->withStatus(401); } // 检查用户状态 if ($user->status !== 1) { return $response->json([ 'code' => 403, 'message' => '账号已被禁用', ])->withStatus(403); } // 生成 Access Token $token = $auth->guard('jwt')->login($user); // 生成 Refresh Token $refreshToken = bin2hex(random_bytes(32)); $user->refresh_token = $refreshToken; $user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->save(); OperationLogService::log( user_id: $user->id, action: 'auth.login', target_type: 'user', target_id: $user->id, description: "用户 {$user->username} 登录", ip: RequestLogMiddleware::getClientIp($request), ); return [ 'code' => 0, 'message' => '登录成功', 'data' => [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'token_type' => 'Bearer', 'expires_in' => 7200, 'user' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, ], ], ]; } /** * 刷新 Access Token */ #[OA\Get( path: '/refresh', summary: '刷新 Access Token', description: '使用 refresh_token 获取新的 access_token,同时轮换 refresh_token', security: [['bearerAuth' => []]], tags: ['Auth'], parameters: [ new OA\Parameter(name: 'refresh_token', in: 'query', required: true, description: 'Refresh Token', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( response: 200, description: 'Token 刷新成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: 'Token 刷新成功'), new OA\Property(property: 'data', properties: [ new OA\Property(property: 'access_token', type: 'string'), new OA\Property(property: 'refresh_token', type: 'string'), new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'), new OA\Property(property: 'expires_in', type: 'integer', example: 7200), ], type: 'object'), ]) ), new OA\Response(response: 400, description: '缺少 refresh_token 参数', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 401, description: 'refresh_token 无效或已过期', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "refresh", methods: "GET")] #[Middleware(AuthMiddleware::class)] public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $refreshToken = $request->input('refresh_token'); if (!is_string($refreshToken) || $refreshToken === '') { return $response->json([ 'code' => 400, 'message' => '缺少 refresh_token 参数', ])->withStatus(400); } // 查找用户 $user = User::query()->where('refresh_token', $refreshToken)->first(); if (!$user) { return $response->json([ 'code' => 401, 'message' => '无效的 refresh_token', ])->withStatus(401); } // 验证 refresh token 是否过期 if (!$user->isRefreshTokenValid()) { return $response->json([ 'code' => 401, 'message' => 'refresh_token 已过期,请重新登录', ])->withStatus(401); } // 检查用户状态 if ($user->status !== 1) { return $response->json([ 'code' => 403, 'message' => '账号已被禁用', ])->withStatus(403); } // 生成新的 Access Token $token = $auth->guard('jwt')->login($user); // 生成新的 Refresh Token(轮换以提升安全性) $newRefreshToken = bin2hex(random_bytes(32)); $user->refresh_token = $newRefreshToken; $user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->save(); return [ 'code' => 0, 'message' => 'Token 刷新成功', 'data' => [ 'access_token' => $token, 'refresh_token' => $newRefreshToken, 'token_type' => 'Bearer', 'expires_in' => 7200, ], ]; } /** * 获取当前用户信息 */ #[OA\Get( path: '/me', summary: '获取当前用户信息', security: [['bearerAuth' => []]], tags: ['Auth'], responses: [ new OA\Response( response: 200, description: '获取成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: '获取成功'), new OA\Property(property: 'data', properties: [ new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'username', type: 'string', example: 'admin'), new OA\Property(property: 'email', type: 'string', example: 'admin@example.com'), new OA\Property(property: 'status', type: 'integer', example: 1), new OA\Property(property: 'ext', type: 'object', nullable: true), new OA\Property(property: 'created_at', type: 'string', format: 'date-time'), ], type: 'object'), ]) ), new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "me", methods: "GET")] #[Middleware(AuthMiddleware::class)] public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array { $user = $auth->guard('jwt')->user(); if (!$user) { return $response->json([ 'code' => 401, 'message' => '未授权', ])->withStatus(401); } return [ 'code' => 0, 'message' => '获取成功', 'data' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, 'status' => $user->status, 'ext' => $user->ext, 'created_at' => $user->created_at->toDateTimeString(), ], ]; } /** * 更新个人信息 * * 当前用户更新自己的 email 和 ext 字段 */ #[OA\Put( path: '/me/profile', summary: '更新个人信息', description: '当前用户更新自己的 email 和 ext 字段', security: [['bearerAuth' => []]], tags: ['Auth'], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent(properties: [ new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'new@example.com'), new OA\Property(property: 'ext', type: 'object', nullable: true, example: ['nickname' => 'user']), ]) ), responses: [ new OA\Response( response: 200, description: '更新成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: '个人信息更新成功'), new OA\Property(property: 'data', properties: [ new OA\Property(property: 'id', type: 'integer'), new OA\Property(property: 'username', type: 'string'), new OA\Property(property: 'email', type: 'string'), new OA\Property(property: 'status', type: 'integer'), new OA\Property(property: 'ext', type: 'object', nullable: true), new OA\Property(property: 'created_at', type: 'string', format: 'date-time'), ], type: 'object'), ]) ), new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "me/profile", methods: "PUT")] #[Middleware(AuthMiddleware::class)] public function updateProfile(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $user = $auth->guard('jwt')->user(); if (!$user instanceof User) { return $response->json([ 'code' => 401, 'message' => '未授权', ])->withStatus(401); } $email = $request->input('email'); $ext = $request->input('ext'); $updates = []; if ($email !== null) { if (!is_string($email) || trim($email) === '') { return $response->json([ 'code' => 400, 'message' => '邮箱不能为空', ])->withStatus(400); } $email = trim($email); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return $response->json([ 'code' => 400, 'message' => '邮箱格式不正确', ])->withStatus(400); } if (strlen($email) > 100) { return $response->json([ 'code' => 400, 'message' => '邮箱长度不能超过 100 个字符', ])->withStatus(400); } if (User::query()->where('email', $email)->where('id', '!=', $user->id)->exists()) { return $response->json([ 'code' => 400, 'message' => '邮箱已被注册', ])->withStatus(400); } $updates['email'] = $email; } if ($ext !== null) { if (!is_array($ext)) { return $response->json([ 'code' => 400, 'message' => 'ext 必须为对象', ])->withStatus(400); } $updates['ext'] = $ext; } if ($updates === []) { return $response->json([ 'code' => 400, 'message' => '缺少可更新字段', ])->withStatus(400); } $user->fill($updates); $user->save(); $user->refresh(); return [ 'code' => 0, 'message' => '个人信息更新成功', 'data' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, 'status' => $user->status, 'ext' => $user->ext, 'created_at' => $user->created_at->toDateTimeString(), ], ]; } /** * 修改密码 * * 需验证旧密码,修改成功后清除 refresh_token 强制重新登录 */ #[OA\Put( path: '/me/password', summary: '修改密码', description: '修改当前用户密码,需验证旧密码。修改成功后清除 refresh_token,需重新登录', security: [['bearerAuth' => []]], tags: ['Auth'], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['old_password', 'new_password'], properties: [ new OA\Property(property: 'old_password', type: 'string', example: 'OldPass_1234'), new OA\Property(property: 'new_password', type: 'string', minLength: 6, maxLength: 32, example: 'NewPass_5678'), ] ) ), responses: [ new OA\Response( response: 200, description: '密码修改成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: '密码修改成功,请重新登录'), ]) ), new OA\Response(response: 400, description: '参数校验失败或旧密码不正确', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "me/password", methods: "PUT")] #[Middleware(AuthMiddleware::class)] public function changePassword(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array { $user = $auth->guard('jwt')->user(); if (!$user instanceof User) { return $response->json([ 'code' => 401, 'message' => '未授权', ])->withStatus(401); } $old_password = $request->input('old_password'); $new_password = $request->input('new_password'); // 验证旧密码 if (!is_string($old_password) || $old_password === '') { return $response->json([ 'code' => 400, 'message' => '旧密码不能为空', ])->withStatus(400); } if (!$user->verifyPassword($old_password)) { return $response->json([ 'code' => 400, 'message' => '旧密码不正确', ])->withStatus(400); } // 验证新密码 if (!is_string($new_password) || $new_password === '') { return $response->json([ 'code' => 400, 'message' => '新密码不能为空', ])->withStatus(400); } $new_password_length = strlen($new_password); if ($new_password_length < 6 || $new_password_length > 32) { return $response->json([ 'code' => 400, 'message' => '新密码长度需在 6-32 个字符', ])->withStatus(400); } // 修改密码并清除 refresh_token $user->password = $new_password; $user->refresh_token = null; $user->refresh_token_expires_at = null; $user->save(); OperationLogService::log( user_id: $user->id, action: 'user.password_change', target_type: 'user', target_id: $user->id, description: "用户 {$user->username} 修改密码", ip: OperationLogService::getRequestIp(), ); return [ 'code' => 0, 'message' => '密码修改成功,请重新登录', ]; } /** * 退出登录 */ #[OA\Get( path: '/logout', summary: '退出登录', description: '退出登录,清除 refresh_token 并注销当前 JWT token', security: [['bearerAuth' => []]], tags: ['Auth'], responses: [ new OA\Response( response: 200, description: '退出成功', content: new OA\JsonContent(properties: [ new OA\Property(property: 'code', type: 'integer', example: 0), new OA\Property(property: 'message', type: 'string', example: '退出成功'), ]) ), new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), ] )] #[RequestMapping(path: "logout", methods: "GET")] #[Middleware(AuthMiddleware::class)] public function logout(AuthManager $auth): array { $user = $auth->guard('jwt')->user(); if ($user instanceof User) { OperationLogService::log( user_id: $user->id, action: 'auth.logout', target_type: 'user', target_id: $user->id, description: "用户 {$user->username} 退出登录", ip: OperationLogService::getRequestIp(), ); // 清除 refresh token $user->refresh_token = null; $user->refresh_token_expires_at = null; $user->save(); } // 注销当前 token $auth->guard('jwt')->logout(); return [ 'code' => 0, 'message' => '退出成功', ]; } }