Files
datahub/backend/app/Controller/api/v1/AuthController.php
T

709 lines
27 KiB
PHP
Raw Normal View History

2025-11-10 10:45:43 +08:00
<?php
declare(strict_types=1);
2026-03-06 15:29:04 +08:00
namespace App\Controller\Api\V1;
2025-11-10 10:45:43 +08:00
2026-03-06 15:29:04 +08:00
use App\Controller\AbstractController;
2026-03-09 10:13:08 +08:00
use App\Middleware\AuthMiddleware;
2025-11-10 10:45:43 +08:00
use App\Model\User;
2026-03-17 15:54:23 +08:00
use App\Service\OperationLogService;
2026-03-18 08:49:10 +08:00
use App\Utils\RequestHelper;
2025-11-10 10:45:43 +08:00
use Carbon\Carbon;
2025-11-10 15:27:00 +08:00
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
2026-03-09 10:13:08 +08:00
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use OpenApi\Attributes as OA;
use Qbhy\HyperfAuth\AuthManager;
2025-11-10 15:27:00 +08:00
2026-03-09 10:13:08 +08:00
#[OA\Tag(name: 'Auth', description: '认证与个人信息')]
#[Controller(prefix: "/api/v1")]
2025-11-10 10:45:43 +08:00
class AuthController extends AbstractController
{
/**
* 用户注册
*/
2026-03-09 10:13:08 +08:00
#[OA\Post(
path: '/register',
summary: '用户注册',
description: '注册新用户,需提供用户名、密码和邮箱',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['username', 'password', 'email'],
properties: [
new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 20, example: 'new_user'),
new OA\Property(property: 'password', type: 'string', minLength: 6, maxLength: 32, example: 'Pass_1234'),
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'user@example.com'),
]
)
),
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: 'new_user'),
new OA\Property(property: 'email', type: 'string', example: 'user@example.com'),
], type: 'object'),
])
),
new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "register", methods: "POST")]
public function register(RequestInterface $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
2025-11-10 10:45:43 +08:00
{
$username = $request->input('username');
$password = $request->input('password');
$email = $request->input('email');
2026-03-09 10:13:08 +08:00
// 校验 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);
}
// 唯一性检查
2025-11-10 10:45:43 +08:00
if (User::query()->where('username', $username)->exists()) {
return $response->json([
'code' => 400,
'message' => '用户名已存在',
2026-03-09 10:13:08 +08:00
])->withStatus(400);
2025-11-10 10:45:43 +08:00
}
if (User::query()->where('email', $email)->exists()) {
return $response->json([
'code' => 400,
'message' => '邮箱已被注册',
2026-03-09 10:13:08 +08:00
])->withStatus(400);
2025-11-10 10:45:43 +08:00
}
// 创建用户
$user = User::create([
'username' => $username,
2026-03-09 10:13:08 +08:00
'password' => $password,
2025-11-10 10:45:43 +08:00
'email' => $email,
'status' => 1,
]);
2026-03-09 10:13:08 +08:00
return [
2025-11-10 10:45:43 +08:00
'code' => 0,
'message' => '注册成功',
'data' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
],
2026-03-09 10:13:08 +08:00
];
2025-11-10 10:45:43 +08:00
}
/**
* 用户登录
*/
2026-03-09 10:13:08 +08:00
#[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
2025-11-10 10:45:43 +08:00
{
$username = $request->input('username');
$password = $request->input('password');
2026-03-09 10:13:08 +08:00
// 校验参数
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);
}
2025-11-10 10:45:43 +08:00
// 查找用户
2026-03-09 10:13:08 +08:00
$user = User::query()->where('username', trim($username))->first();
2025-11-10 10:45:43 +08:00
if (!$user) {
return $response->json([
'code' => 401,
'message' => '用户名或密码错误',
2026-03-09 10:13:08 +08:00
])->withStatus(401);
2025-11-10 10:45:43 +08:00
}
// 验证密码
if (!$user->verifyPassword($password)) {
return $response->json([
'code' => 401,
'message' => '用户名或密码错误',
2026-03-09 10:13:08 +08:00
])->withStatus(401);
2025-11-10 10:45:43 +08:00
}
// 检查用户状态
if ($user->status !== 1) {
return $response->json([
'code' => 403,
'message' => '账号已被禁用',
2026-03-09 10:13:08 +08:00
])->withStatus(403);
2025-11-10 10:45:43 +08:00
}
// 生成 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();
2026-03-17 15:54:23 +08:00
OperationLogService::log(
user_id: $user->id,
action: 'auth.login',
target_type: 'user',
target_id: $user->id,
description: "用户 {$user->username} 登录",
2026-03-18 08:49:10 +08:00
ip: RequestHelper::getClientIp($this->request),
2026-03-17 15:54:23 +08:00
);
2026-03-09 10:13:08 +08:00
return [
2025-11-10 10:45:43 +08:00
'code' => 0,
'message' => '登录成功',
'data' => [
'access_token' => $token,
'refresh_token' => $refreshToken,
'token_type' => 'Bearer',
2026-03-09 10:13:08 +08:00
'expires_in' => 7200,
2025-11-10 10:45:43 +08:00
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
],
],
2026-03-09 10:13:08 +08:00
];
2025-11-10 10:45:43 +08:00
}
/**
* 刷新 Access Token
*/
2026-03-09 10:13:08 +08:00
#[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")]
2025-11-10 15:27:00 +08:00
#[Middleware(AuthMiddleware::class)]
2026-03-09 10:13:08 +08:00
public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
2025-11-10 10:45:43 +08:00
{
$refreshToken = $request->input('refresh_token');
2026-03-09 10:13:08 +08:00
if (!is_string($refreshToken) || $refreshToken === '') {
2025-11-10 10:45:43 +08:00
return $response->json([
'code' => 400,
'message' => '缺少 refresh_token 参数',
2026-03-09 10:13:08 +08:00
])->withStatus(400);
2025-11-10 10:45:43 +08:00
}
// 查找用户
$user = User::query()->where('refresh_token', $refreshToken)->first();
if (!$user) {
return $response->json([
'code' => 401,
'message' => '无效的 refresh_token',
2026-03-09 10:13:08 +08:00
])->withStatus(401);
2025-11-10 10:45:43 +08:00
}
// 验证 refresh token 是否过期
if (!$user->isRefreshTokenValid()) {
return $response->json([
'code' => 401,
'message' => 'refresh_token 已过期,请重新登录',
2026-03-09 10:13:08 +08:00
])->withStatus(401);
2025-11-10 10:45:43 +08:00
}
// 检查用户状态
if ($user->status !== 1) {
return $response->json([
'code' => 403,
'message' => '账号已被禁用',
2026-03-09 10:13:08 +08:00
])->withStatus(403);
2025-11-10 10:45:43 +08:00
}
// 生成新的 Access Token
$token = $auth->guard('jwt')->login($user);
2026-03-09 10:13:08 +08:00
// 生成新的 Refresh Token(轮换以提升安全性)
2025-11-10 10:45:43 +08:00
$newRefreshToken = bin2hex(random_bytes(32));
$user->refresh_token = $newRefreshToken;
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
$user->save();
2026-03-09 10:13:08 +08:00
return [
2025-11-10 10:45:43 +08:00
'code' => 0,
'message' => 'Token 刷新成功',
'data' => [
'access_token' => $token,
'refresh_token' => $newRefreshToken,
'token_type' => 'Bearer',
'expires_in' => 7200,
],
2026-03-09 10:13:08 +08:00
];
2025-11-10 10:45:43 +08:00
}
/**
* 获取当前用户信息
*/
2026-03-09 10:13:08 +08:00
#[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")]
2025-11-10 15:27:00 +08:00
#[Middleware(AuthMiddleware::class)]
2026-03-09 10:13:08 +08:00
public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
2025-11-10 10:45:43 +08:00
{
$user = $auth->guard('jwt')->user();
if (!$user) {
return $response->json([
'code' => 401,
'message' => '未授权',
2026-03-09 10:13:08 +08:00
])->withStatus(401);
2025-11-10 10:45:43 +08:00
}
2026-03-09 10:13:08 +08:00
return [
2025-11-10 10:45:43 +08:00
'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(),
],
2026-03-09 10:13:08 +08:00
];
}
/**
* 更新个人信息
*
* 当前用户更新自己的 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();
2026-03-17 15:54:23 +08:00
OperationLogService::log(
user_id: $user->id,
action: 'user.password_change',
target_type: 'user',
target_id: $user->id,
description: "用户 {$user->username} 修改密码",
2026-03-18 08:49:10 +08:00
ip: RequestHelper::getClientIp($this->request),
2026-03-17 15:54:23 +08:00
);
2026-03-09 10:13:08 +08:00
return [
'code' => 0,
'message' => '密码修改成功,请重新登录',
];
2025-11-10 10:45:43 +08:00
}
/**
* 退出登录
*/
2026-03-09 10:13:08 +08:00
#[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")]
2025-11-10 15:27:00 +08:00
#[Middleware(AuthMiddleware::class)]
2026-03-09 10:13:08 +08:00
public function logout(AuthManager $auth): array
2025-11-10 10:45:43 +08:00
{
$user = $auth->guard('jwt')->user();
if ($user instanceof User) {
2026-03-17 15:54:23 +08:00
OperationLogService::log(
user_id: $user->id,
action: 'auth.logout',
target_type: 'user',
target_id: $user->id,
description: "用户 {$user->username} 退出登录",
2026-03-18 08:49:10 +08:00
ip: RequestHelper::getClientIp($this->request),
2026-03-17 15:54:23 +08:00
);
2025-11-10 10:45:43 +08:00
// 清除 refresh token
$user->refresh_token = null;
$user->refresh_token_expires_at = null;
$user->save();
}
// 注销当前 token
$auth->guard('jwt')->logout();
2026-03-09 10:13:08 +08:00
return [
2025-11-10 10:45:43 +08:00
'code' => 0,
'message' => '退出成功',
2026-03-09 10:13:08 +08:00
];
2025-11-10 10:45:43 +08:00
}
}