update auth controller

This commit is contained in:
2026-03-09 10:13:08 +08:00
parent f792c04002
commit 42b68f51f6
+478 -43
View File
@@ -5,54 +5,144 @@ declare(strict_types=1);
namespace App\Controller\Api\V1; namespace App\Controller\Api\V1;
use App\Controller\AbstractController; use App\Controller\AbstractController;
use App\Middleware\AuthMiddleware;
use App\Model\User; use App\Model\User;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Qbhy\HyperfAuth\AuthManager;
use Carbon\Carbon; use Carbon\Carbon;
use Hyperf\HttpServer\Annotation\Controller; use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\HttpServer\Annotation\Middleware; use Hyperf\HttpServer\Annotation\Middleware;
use Qbhy\HyperfAuth\AuthMiddleware; use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use OpenApi\Attributes as OA;
use Qbhy\HyperfAuth\AuthManager;
#[OA\Tag(name: 'Auth', description: '认证与个人信息')]
#[Controller] #[Controller(prefix: "/api/v1")]
class AuthController extends AbstractController class AuthController extends AbstractController
{ {
/** /**
* 用户注册 * 用户注册
*/ */
#[RequestMapping(path:'/register', methods:'post')] #[OA\Post(
public function register(RequestInterface $request, ResponseInterface $response) 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
{ {
$username = $request->input('username'); $username = $request->input('username');
$password = $request->input('password'); $password = $request->input('password');
$email = $request->input('email'); $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()) { if (User::query()->where('username', $username)->exists()) {
return $response->json([ return $response->json([
'code' => 400, 'code' => 400,
'message' => '用户名已存在', 'message' => '用户名已存在',
]); ])->withStatus(400);
} }
if (User::query()->where('email', $email)->exists()) { if (User::query()->where('email', $email)->exists()) {
return $response->json([ return $response->json([
'code' => 400, 'code' => 400,
'message' => '邮箱已被注册', 'message' => '邮箱已被注册',
]); ])->withStatus(400);
} }
// 创建用户 // 创建用户
$user = User::create([ $user = User::create([
'username' => $username, 'username' => $username,
'password' => $password, // 自动加密 'password' => $password,
'email' => $email, 'email' => $email,
'status' => 1, 'status' => 1,
]); ]);
return $response->json([ return [
'code' => 0, 'code' => 0,
'message' => '注册成功', 'message' => '注册成功',
'data' => [ 'data' => [
@@ -60,26 +150,81 @@ class AuthController extends AbstractController
'username' => $user->username, 'username' => $user->username,
'email' => $user->email, 'email' => $user->email,
], ],
]); ];
} }
/** /**
* 用户登录 * 用户登录
*/ */
#[RequestMapping(path:'/login', methods:'post')] #[OA\Post(
public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth) 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'); $username = $request->input('username');
$password = $request->input('password'); $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', $username)->first(); $user = User::query()->where('username', trim($username))->first();
if (!$user) { if (!$user) {
return $response->json([ return $response->json([
'code' => 401, 'code' => 401,
'message' => '用户名或密码错误', 'message' => '用户名或密码错误',
]); ])->withStatus(401);
} }
// 验证密码 // 验证密码
@@ -87,7 +232,7 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 401, 'code' => 401,
'message' => '用户名或密码错误', 'message' => '用户名或密码错误',
]); ])->withStatus(401);
} }
// 检查用户状态 // 检查用户状态
@@ -95,7 +240,7 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 403, 'code' => 403,
'message' => '账号已被禁用', 'message' => '账号已被禁用',
]); ])->withStatus(403);
} }
// 生成 Access Token // 生成 Access Token
@@ -107,37 +252,66 @@ class AuthController extends AbstractController
$user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->refresh_token_expires_at = Carbon::now()->addDays(30);
$user->save(); $user->save();
return $response->json([ return [
'code' => 0, 'code' => 0,
'message' => '登录成功', 'message' => '登录成功',
'data' => [ 'data' => [
'access_token' => $token, 'access_token' => $token,
'refresh_token' => $refreshToken, 'refresh_token' => $refreshToken,
'token_type' => 'Bearer', 'token_type' => 'Bearer',
'expires_in' => 7200, // 2 小时 'expires_in' => 7200,
'user' => [ 'user' => [
'id' => $user->id, 'id' => $user->id,
'username' => $user->username, 'username' => $user->username,
'email' => $user->email, 'email' => $user->email,
], ],
], ],
]); ];
} }
/** /**
* 刷新 Access Token * 刷新 Access Token
*/ */
#[RequestMapping(path:'/refresh', methods:'get')] #[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)] #[Middleware(AuthMiddleware::class)]
public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth) public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
{ {
$refreshToken = $request->input('refresh_token'); $refreshToken = $request->input('refresh_token');
if (!$refreshToken) { if (!is_string($refreshToken) || $refreshToken === '') {
return $response->json([ return $response->json([
'code' => 400, 'code' => 400,
'message' => '缺少 refresh_token 参数', 'message' => '缺少 refresh_token 参数',
]); ])->withStatus(400);
} }
// 查找用户 // 查找用户
@@ -147,7 +321,7 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 401, 'code' => 401,
'message' => '无效的 refresh_token', 'message' => '无效的 refresh_token',
]); ])->withStatus(401);
} }
// 验证 refresh token 是否过期 // 验证 refresh token 是否过期
@@ -155,7 +329,7 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 401, 'code' => 401,
'message' => 'refresh_token 已过期,请重新登录', 'message' => 'refresh_token 已过期,请重新登录',
]); ])->withStatus(401);
} }
// 检查用户状态 // 检查用户状态
@@ -163,19 +337,19 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 403, 'code' => 403,
'message' => '账号已被禁用', 'message' => '账号已被禁用',
]); ])->withStatus(403);
} }
// 生成新的 Access Token // 生成新的 Access Token
$token = $auth->guard('jwt')->login($user); $token = $auth->guard('jwt')->login($user);
// 可选:生成新的 Refresh Token安全) // 生成新的 Refresh Token轮换以提升安全
$newRefreshToken = bin2hex(random_bytes(32)); $newRefreshToken = bin2hex(random_bytes(32));
$user->refresh_token = $newRefreshToken; $user->refresh_token = $newRefreshToken;
$user->refresh_token_expires_at = Carbon::now()->addDays(30); $user->refresh_token_expires_at = Carbon::now()->addDays(30);
$user->save(); $user->save();
return $response->json([ return [
'code' => 0, 'code' => 0,
'message' => 'Token 刷新成功', 'message' => 'Token 刷新成功',
'data' => [ 'data' => [
@@ -184,15 +358,40 @@ class AuthController extends AbstractController
'token_type' => 'Bearer', 'token_type' => 'Bearer',
'expires_in' => 7200, 'expires_in' => 7200,
], ],
]); ];
} }
/** /**
* 获取当前用户信息 * 获取当前用户信息
*/ */
#[RequestMapping(path:'/me', methods:'get')] #[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)] #[Middleware(AuthMiddleware::class)]
public function me(AuthManager $auth, ResponseInterface $response) public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
{ {
$user = $auth->guard('jwt')->user(); $user = $auth->guard('jwt')->user();
@@ -200,10 +399,10 @@ class AuthController extends AbstractController
return $response->json([ return $response->json([
'code' => 401, 'code' => 401,
'message' => '未授权', 'message' => '未授权',
]); ])->withStatus(401);
} }
return $response->json([ return [
'code' => 0, 'code' => 0,
'message' => '获取成功', 'message' => '获取成功',
'data' => [ 'data' => [
@@ -214,15 +413,251 @@ class AuthController extends AbstractController
'ext' => $user->ext, 'ext' => $user->ext,
'created_at' => $user->created_at->toDateTimeString(), '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();
return [
'code' => 0,
'message' => '密码修改成功,请重新登录',
];
} }
/** /**
* 退出登录 * 退出登录
*/ */
#[RequestMapping(path:'/logout', methods:'get')] #[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)] #[Middleware(AuthMiddleware::class)]
public function logout(AuthManager $auth, ResponseInterface $response) public function logout(AuthManager $auth): array
{ {
$user = $auth->guard('jwt')->user(); $user = $auth->guard('jwt')->user();
@@ -236,9 +671,9 @@ class AuthController extends AbstractController
// 注销当前 token // 注销当前 token
$auth->guard('jwt')->logout(); $auth->guard('jwt')->logout();
return $response->json([ return [
'code' => 0, 'code' => 0,
'message' => '退出成功', 'message' => '退出成功',
]); ];
} }
} }