ThinkPHP8 API开发与安全防护实战指南:JWT认证、XSS/CSRF防护全攻略
Orion K Lv6

在现代Web开发中,API安全是至关重要的环节。ThinkPHP8作为新一代PHP框架,提供了强大的API开发能力和完善的安全机制。本文将深入探讨ThinkPHP8中的API开发技巧、JWT认证实现以及XSS/CSRF等安全防护策略,帮助开发者构建安全可靠的API服务。

ThinkPHP8 API开发基础

多应用模式配置

首先安装多应用模式扩展,支持API应用独立管理:

1
composer require topthink/think-multi-app

创建API应用目录结构:

1
2
3
4
5
6
7
8
9
app/
├── api/ # API应用
│ ├── controller/ # 控制器
│ ├── model/ # 模型
│ ├── middleware/ # 中间件
│ ├── service/ # 服务层
│ └── common.php # 公共函数
├── admin/ # 后台应用
└── index/ # 前台应用

API基础控制器

创建API基础控制器,统一处理响应格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php
declare(strict_types=1);

namespace app\api\controller;

use think\App;
use think\Request;
use think\Response;
use think\exception\HttpResponseException;

/**
* API基础控制器
* 提供统一的API响应格式和基础功能
*/
abstract class BaseController
{
/**
* Request实例
* @var Request
*/
protected $request;

/**
* 应用实例
* @var App
*/
protected $app;

/**
* 构造方法
* @param App $app 应用实例
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;

// 控制器初始化
$this->initialize();
}

/**
* 初始化方法
* @return void
*/
protected function initialize(): void
{
// 设置跨域头
$this->setCorsHeaders();
}

/**
* 设置跨域响应头
* @return void
*/
protected function setCorsHeaders(): void
{
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Max-Age: 86400');

// 处理预检请求
if ($this->request->method() === 'OPTIONS') {
exit();
}
}

/**
* 成功响应
* @param mixed $data 响应数据
* @param string $message 响应消息
* @param int $code 状态码
* @return Response
*/
protected function success($data = [], string $message = 'success', int $code = 200): Response
{
return $this->response([
'code' => $code,
'message' => $message,
'data' => $data,
'timestamp' => time()
], $code);
}

/**
* 错误响应
* @param string $message 错误消息
* @param int $code 错误码
* @param mixed $data 错误数据
* @return Response
*/
protected function error(string $message = 'error', int $code = 400, $data = []): Response
{
return $this->response([
'code' => $code,
'message' => $message,
'data' => $data,
'timestamp' => time()
], $code);
}

/**
* 统一响应方法
* @param array $data 响应数据
* @param int $httpCode HTTP状态码
* @return Response
*/
protected function response(array $data, int $httpCode = 200): Response
{
return Response::create($data, 'json', $httpCode);
}

/**
* 参数验证
* @param array $rules 验证规则
* @param array $data 验证数据
* @param array $messages 错误消息
* @return array 验证后的数据
* @throws HttpResponseException
*/
protected function validate(array $rules, array $data = [], array $messages = []): array
{
$data = $data ?: $this->request->param();

$validate = \think\facade\Validate::make($rules, $messages);

if (!$validate->check($data)) {
throw new HttpResponseException(
$this->error($validate->getError(), 422)
);
}

return $data;
}

/**
* 获取当前用户ID
* @return int|null
*/
protected function getUserId(): ?int
{
return $this->request->user_id ?? null;
}

/**
* 获取当前用户信息
* @return array|null
*/
protected function getUser(): ?array
{
return $this->request->user ?? null;
}
}

JWT认证系统实现

安装JWT扩展

1
composer require firebase/php-jwt

JWT服务类

创建JWT服务类,处理token的生成、验证和刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
<?php
declare(strict_types=1);

namespace app\api\service;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use think\facade\Cache;
use think\facade\Config;
use think\facade\Log;

/**
* JWT认证服务
* 处理JWT token的生成、验证、刷新等操作
*/
class JwtService
{
/**
* JWT密钥
* @var string
*/
private string $key;

/**
* 算法
* @var string
*/
private string $algorithm;

/**
* 过期时间(秒)
* @var int
*/
private int $ttl;

/**
* 刷新时间(秒)
* @var int
*/
private int $refreshTtl;

/**
* 构造函数
*/
public function __construct()
{
$this->key = Config::get('jwt.key', 'your-secret-key');
$this->algorithm = Config::get('jwt.algorithm', 'HS256');
$this->ttl = Config::get('jwt.ttl', 7200); // 2小时
$this->refreshTtl = Config::get('jwt.refresh_ttl', 604800); // 7天
}

/**
* 生成JWT token
* @param array $payload 载荷数据
* @return array 包含access_token和refresh_token
*/
public function generateToken(array $payload): array
{
$now = time();
$jti = $this->generateJti();

// Access Token载荷
$accessPayload = array_merge($payload, [
'iat' => $now, // 签发时间
'exp' => $now + $this->ttl, // 过期时间
'jti' => $jti, // JWT ID
'type' => 'access' // token类型
]);

// Refresh Token载荷
$refreshPayload = [
'user_id' => $payload['user_id'] ?? 0,
'iat' => $now,
'exp' => $now + $this->refreshTtl,
'jti' => $jti . '_refresh',
'type' => 'refresh'
];

$accessToken = JWT::encode($accessPayload, $this->key, $this->algorithm);
$refreshToken = JWT::encode($refreshPayload, $this->key, $this->algorithm);

// 将refresh token存储到缓存中
Cache::set('refresh_token:' . $jti, $refreshToken, $this->refreshTtl);

return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'token_type' => 'Bearer',
'expires_in' => $this->ttl
];
}

/**
* 验证JWT token
* @param string $token JWT token
* @return array|false 解码后的载荷数据或false
*/
public function verifyToken(string $token)
{
try {
$decoded = JWT::decode($token, new Key($this->key, $this->algorithm));
$payload = (array) $decoded;

// 检查token是否在黑名单中
if ($this->isTokenBlacklisted($payload['jti'] ?? '')) {
return false;
}

return $payload;
} catch (ExpiredException $e) {
Log::info('JWT token已过期', ['token' => substr($token, 0, 20) . '...']);
return false;
} catch (SignatureInvalidException $e) {
Log::warning('JWT token签名无效', ['token' => substr($token, 0, 20) . '...']);
return false;
} catch (\Exception $e) {
Log::error('JWT token验证失败', [
'error' => $e->getMessage(),
'token' => substr($token, 0, 20) . '...'
]);
return false;
}
}

/**
* 刷新JWT token
* @param string $refreshToken 刷新token
* @return array|false 新的token信息或false
*/
public function refreshToken(string $refreshToken)
{
$payload = $this->verifyToken($refreshToken);

if (!$payload || ($payload['type'] ?? '') !== 'refresh') {
return false;
}

$jti = str_replace('_refresh', '', $payload['jti'] ?? '');

// 检查refresh token是否存在于缓存中
if (!Cache::get('refresh_token:' . $jti)) {
return false;
}

// 生成新的token
$newPayload = [
'user_id' => $payload['user_id'],
'username' => $payload['username'] ?? '',
'role' => $payload['role'] ?? 'user'
];

// 删除旧的refresh token
Cache::delete('refresh_token:' . $jti);

return $this->generateToken($newPayload);
}

/**
* 注销token(加入黑名单)
* @param string $token JWT token
* @return bool 是否成功
*/
public function revokeToken(string $token): bool
{
$payload = $this->verifyToken($token);

if (!$payload) {
return false;
}

$jti = $payload['jti'] ?? '';
$exp = $payload['exp'] ?? 0;

// 将token加入黑名单,过期时间为token的剩余有效期
$ttl = max(0, $exp - time());
Cache::set('blacklist:' . $jti, true, $ttl);

// 删除对应的refresh token
$refreshJti = str_replace('_refresh', '', $jti);
Cache::delete('refresh_token:' . $refreshJti);

return true;
}

/**
* 检查token是否在黑名单中
* @param string $jti JWT ID
* @return bool
*/
private function isTokenBlacklisted(string $jti): bool
{
return Cache::has('blacklist:' . $jti);
}

/**
* 生成JWT ID
* @return string
*/
private function generateJti(): string
{
return md5(uniqid() . microtime(true) . mt_rand());
}

/**
* 从请求头中提取token
* @param string $header Authorization头
* @return string|null
*/
public function extractTokenFromHeader(string $header): ?string
{
if (preg_match('/Bearer\s+(\S+)/', $header, $matches)) {
return $matches[1];
}

return null;
}
}

JWT认证中间件

创建JWT认证中间件,自动验证API请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?php
declare(strict_types=1);

namespace app\api\middleware;

use app\api\service\JwtService;
use think\Request;
use think\Response;
use Closure;

/**
* JWT认证中间件
* 验证API请求中的JWT token
*/
class JwtAuth
{
/**
* JWT服务
* @var JwtService
*/
private JwtService $jwtService;

/**
* 构造函数
*/
public function __construct()
{
$this->jwtService = new JwtService();
}

/**
* 处理请求
* @param Request $request 请求对象
* @param Closure $next 下一个中间件
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
// 获取Authorization头
$authorization = $request->header('Authorization', '');

if (empty($authorization)) {
return $this->unauthorizedResponse('缺少Authorization头');
}

// 提取token
$token = $this->jwtService->extractTokenFromHeader($authorization);

if (!$token) {
return $this->unauthorizedResponse('无效的Authorization格式');
}

// 验证token
$payload = $this->jwtService->verifyToken($token);

if (!$payload) {
return $this->unauthorizedResponse('无效或已过期的token');
}

// 检查token类型
if (($payload['type'] ?? '') !== 'access') {
return $this->unauthorizedResponse('无效的token类型');
}

// 将用户信息注入到请求中
$request->user_id = $payload['user_id'] ?? 0;
$request->user = [
'id' => $payload['user_id'] ?? 0,
'username' => $payload['username'] ?? '',
'role' => $payload['role'] ?? 'user'
];

return $next($request);
}

/**
* 返回未授权响应
* @param string $message 错误消息
* @return Response
*/
private function unauthorizedResponse(string $message): Response
{
return Response::create([
'code' => 401,
'message' => $message,
'data' => [],
'timestamp' => time()
], 'json', 401);
}
}

安全防护机制

XSS防护实现

XSS(跨站脚本攻击)防护是API安全的重要环节 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php
declare(strict_types=1);

namespace app\api\service;

use think\facade\Log;

/**
* XSS防护服务
* 提供输入数据的XSS过滤和安全检查
*/
class XssProtectionService
{
/**
* 危险标签列表
* @var array
*/
private array $dangerousTags = [
'script', 'iframe', 'object', 'embed', 'form', 'input',
'textarea', 'button', 'select', 'option', 'link', 'style'
];

/**
* 危险属性列表
* @var array
*/
private array $dangerousAttributes = [
'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout',
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset',
'javascript:', 'vbscript:', 'data:', 'expression('
];

/**
* 过滤XSS攻击
* @param mixed $data 需要过滤的数据
* @param bool $strict 是否严格模式
* @return mixed 过滤后的数据
*/
public function filter($data, bool $strict = true)
{
if (is_array($data)) {
return array_map(function($item) use ($strict) {
return $this->filter($item, $strict);
}, $data);
}

if (!is_string($data)) {
return $data;
}

// 记录原始数据用于日志
$originalData = $data;

// 基础HTML实体编码
$data = htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');

if ($strict) {
// 严格模式:移除所有HTML标签
$data = strip_tags($data);
} else {
// 宽松模式:只移除危险标签和属性
$data = $this->removeDangerousTags($data);
$data = $this->removeDangerousAttributes($data);
}

// 移除潜在的脚本注入
$data = $this->removeScriptInjection($data);

// 如果数据被修改,记录日志
if ($originalData !== $data) {
Log::warning('检测到潜在XSS攻击', [
'original' => substr($originalData, 0, 200),
'filtered' => substr($data, 0, 200),
'ip' => request()->ip(),
'user_agent' => request()->header('User-Agent')
]);
}

return $data;
}

/**
* 移除危险HTML标签
* @param string $data 输入数据
* @return string 处理后的数据
*/
private function removeDangerousTags(string $data): string
{
foreach ($this->dangerousTags as $tag) {
$pattern = '/<\s*' . preg_quote($tag, '/') . '[^>]*>.*?<\s*\/\s*' . preg_quote($tag, '/') . '\s*>/is';
$data = preg_replace($pattern, '', $data);

// 移除自闭合标签
$pattern = '/<\s*' . preg_quote($tag, '/') . '[^>]*\/>/is';
$data = preg_replace($pattern, '', $data);
}

return $data;
}

/**
* 移除危险属性
* @param string $data 输入数据
* @return string 处理后的数据
*/
private function removeDangerousAttributes(string $data): string
{
foreach ($this->dangerousAttributes as $attr) {
if (strpos($attr, ':') !== false || strpos($attr, '(') !== false) {
// 处理协议和函数调用
$pattern = '/' . preg_quote($attr, '/') . '/i';
$data = preg_replace($pattern, '', $data);
} else {
// 处理事件属性
$pattern = '/' . preg_quote($attr, '/') . '\s*=\s*["\'][^"\'>]*["\'][^>]*/i';
$data = preg_replace($pattern, '', $data);
}
}

return $data;
}

/**
* 移除脚本注入
* @param string $data 输入数据
* @return string 处理后的数据
*/
private function removeScriptInjection(string $data): string
{
// 移除各种编码的script标签
$patterns = [
'/&lt;script[^&]*&gt;.*?&lt;\/script&gt;/is',
'/\\u003cscript[^\\]*\\u003e.*?\\u003c\/script\\u003e/is',
'/%3Cscript[^%]*%3E.*?%3C%2Fscript%3E/is',
];

foreach ($patterns as $pattern) {
$data = preg_replace($pattern, '', $data);
}

return $data;
}

/**
* 检查是否包含XSS攻击
* @param string $data 检查的数据
* @return bool 是否包含XSS
*/
public function containsXss(string $data): bool
{
$originalData = $data;
$filteredData = $this->filter($data, false);

return $originalData !== $filteredData;
}

/**
* 验证URL安全性
* @param string $url URL地址
* @return bool 是否安全
*/
public function isUrlSafe(string $url): bool
{
// 检查协议
$allowedProtocols = ['http', 'https', 'ftp', 'ftps'];
$protocol = parse_url($url, PHP_URL_SCHEME);

if (!in_array(strtolower($protocol), $allowedProtocols)) {
return false;
}

// 检查是否包含危险字符
$dangerousPatterns = [
'/javascript:/i',
'/vbscript:/i',
'/data:/i',
'/file:/i'
];

foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $url)) {
return false;
}
}

return true;
}
}

CSRF防护实现

CSRF(跨站请求伪造)防护通过验证请求来源和token实现 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<?php
declare(strict_types=1);

namespace app\api\middleware;

use think\Request;
use think\Response;
use think\facade\Cache;
use think\facade\Log;
use Closure;

/**
* CSRF防护中间件
* 防止跨站请求伪造攻击
*/
class CsrfProtection
{
/**
* 需要CSRF保护的HTTP方法
* @var array
*/
private array $protectedMethods = ['POST', 'PUT', 'DELETE', 'PATCH'];

/**
* 白名单路由(不需要CSRF保护)
* @var array
*/
private array $whitelist = [
'/api/auth/login',
'/api/auth/register',
'/api/webhook/*'
];

/**
* 处理请求
* @param Request $request 请求对象
* @param Closure $next 下一个中间件
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
// 检查是否需要CSRF保护
if (!$this->shouldProtect($request)) {
return $next($request);
}

// 验证Referer
if (!$this->verifyReferer($request)) {
return $this->forbiddenResponse('无效的请求来源');
}

// 验证CSRF Token
if (!$this->verifyCsrfToken($request)) {
return $this->forbiddenResponse('无效的CSRF Token');
}

return $next($request);
}

/**
* 检查是否需要CSRF保护
* @param Request $request 请求对象
* @return bool
*/
private function shouldProtect(Request $request): bool
{
// 检查HTTP方法
if (!in_array($request->method(), $this->protectedMethods)) {
return false;
}

// 检查白名单
$path = $request->pathinfo();
foreach ($this->whitelist as $pattern) {
if ($this->matchPattern($pattern, $path)) {
return false;
}
}

return true;
}

/**
* 验证Referer头
* @param Request $request 请求对象
* @return bool
*/
private function verifyReferer(Request $request): bool
{
$referer = $request->header('Referer', '');

// 如果没有Referer,检查是否允许
if (empty($referer)) {
// 对于API请求,可以允许空Referer
return true;
}

$refererHost = parse_url($referer, PHP_URL_HOST);
$currentHost = $request->host();

// 验证Referer域名
if ($refererHost !== $currentHost) {
Log::warning('CSRF攻击检测:Referer不匹配', [
'referer' => $referer,
'current_host' => $currentHost,
'ip' => $request->ip(),
'user_agent' => $request->header('User-Agent')
]);

return false;
}

return true;
}

/**
* 验证CSRF Token
* @param Request $request 请求对象
* @return bool
*/
private function verifyCsrfToken(Request $request): bool
{
// 从多个位置获取CSRF token
$token = $request->header('X-CSRF-Token')
?: $request->header('X-XSRF-Token')
?: $request->param('_token')
?: $request->param('csrf_token');

if (empty($token)) {
return false;
}

// 验证token格式和有效性
return $this->validateToken($token, $request);
}

/**
* 验证token有效性
* @param string $token CSRF token
* @param Request $request 请求对象
* @return bool
*/
private function validateToken(string $token, Request $request): bool
{
// 解析token
$parts = explode(':', base64_decode($token));

if (count($parts) !== 3) {
return false;
}

[$timestamp, $userId, $hash] = $parts;

// 检查时间戳(token有效期1小时)
if (time() - (int)$timestamp > 3600) {
return false;
}

// 验证hash
$expectedHash = hash_hmac('sha256', $timestamp . ':' . $userId, $this->getSecretKey());

if (!hash_equals($expectedHash, $hash)) {
return false;
}

// 检查用户ID是否匹配(如果有用户认证)
$currentUserId = $request->user_id ?? 0;
if ($currentUserId > 0 && (int)$userId !== $currentUserId) {
return false;
}

return true;
}

/**
* 生成CSRF Token
* @param int $userId 用户ID
* @return string
*/
public function generateToken(int $userId = 0): string
{
$timestamp = time();
$hash = hash_hmac('sha256', $timestamp . ':' . $userId, $this->getSecretKey());

return base64_encode($timestamp . ':' . $userId . ':' . $hash);
}

/**
* 获取密钥
* @return string
*/
private function getSecretKey(): string
{
return config('app.key', 'default-secret-key');
}

/**
* 匹配路径模式
* @param string $pattern 模式
* @param string $path 路径
* @return bool
*/
private function matchPattern(string $pattern, string $path): bool
{
$pattern = str_replace('*', '.*', preg_quote($pattern, '/'));
return preg_match('/^' . $pattern . '$/', $path);
}

/**
* 返回禁止访问响应
* @param string $message 错误消息
* @return Response
*/
private function forbiddenResponse(string $message): Response
{
return Response::create([
'code' => 403,
'message' => $message,
'data' => [],
'timestamp' => time()
], 'json', 403);
}
}

API接口实战示例

用户认证接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
<?php
declare(strict_types=1);

namespace app\api\controller;

use app\api\service\JwtService;
use app\api\service\XssProtectionService;
use app\api\model\User;
use think\facade\Db;
use think\facade\Log;

/**
* 用户认证控制器
* 处理用户登录、注册、注销等认证相关操作
*/
class AuthController extends BaseController
{
/**
* JWT服务
* @var JwtService
*/
private JwtService $jwtService;

/**
* XSS防护服务
* @var XssProtectionService
*/
private XssProtectionService $xssService;

/**
* 初始化
* @return void
*/
protected function initialize(): void
{
parent::initialize();
$this->jwtService = new JwtService();
$this->xssService = new XssProtectionService();
}

/**
* 用户登录
* @return \think\Response
*/
public function login()
{
// 验证参数
$data = $this->validate([
'username|用户名' => 'require|length:3,20',
'password|密码' => 'require|length:6,32'
]);

// XSS过滤
$data = $this->xssService->filter($data);

try {
// 查找用户
$user = User::where('username', $data['username'])
->whereOr('email', $data['username'])
->whereOr('mobile', $data['username'])
->find();

if (!$user) {
return $this->error('用户不存在', 404);
}

// 验证密码
if (!password_verify($data['password'], $user->password)) {
// 记录登录失败
$this->logLoginAttempt($data['username'], false);
return $this->error('密码错误', 401);
}

// 检查用户状态
if ($user->status !== 1) {
return $this->error('账户已被禁用', 403);
}

// 生成JWT token
$tokenData = $this->jwtService->generateToken([
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role
]);

// 更新登录信息
$user->save([
'last_login_time' => date('Y-m-d H:i:s'),
'last_login_ip' => $this->request->ip()
]);

// 记录登录成功
$this->logLoginAttempt($data['username'], true, $user->id);

return $this->success([
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'role' => $user->role
],
'token' => $tokenData
], '登录成功');

} catch (\Exception $e) {
Log::error('用户登录异常', [
'username' => $data['username'],
'error' => $e->getMessage(),
'ip' => $this->request->ip()
]);

return $this->error('登录失败,请稍后重试', 500);
}
}

/**
* 用户注册
* @return \think\Response
*/
public function register()
{
// 验证参数
$data = $this->validate([
'username|用户名' => 'require|length:3,20|unique:user',
'email|邮箱' => 'require|email|unique:user',
'password|密码' => 'require|length:6,32',
'confirm_password|确认密码' => 'require|confirm:password'
]);

// XSS过滤
$data = $this->xssService->filter($data);

// 开启事务
Db::startTrans();

try {
// 创建用户
$user = User::create([
'username' => $data['username'],
'email' => $data['email'],
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'role' => 'user',
'status' => 1,
'created_at' => date('Y-m-d H:i:s'),
'register_ip' => $this->request->ip()
]);

// 提交事务
Db::commit();

Log::info('用户注册成功', [
'user_id' => $user->id,
'username' => $user->username,
'ip' => $this->request->ip()
]);

return $this->success([
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email
]
], '注册成功');

} catch (\Exception $e) {
Db::rollback();

Log::error('用户注册失败', [
'username' => $data['username'],
'error' => $e->getMessage(),
'ip' => $this->request->ip()
]);

return $this->error('注册失败,请稍后重试', 500);
}
}

/**
* 刷新token
* @return \think\Response
*/
public function refresh()
{
$refreshToken = $this->request->param('refresh_token');

if (empty($refreshToken)) {
return $this->error('缺少refresh_token参数', 400);
}

$tokenData = $this->jwtService->refreshToken($refreshToken);

if (!$tokenData) {
return $this->error('无效的refresh_token', 401);
}

return $this->success($tokenData, 'Token刷新成功');
}

/**
* 用户注销
* @return \think\Response
*/
public function logout()
{
$authorization = $this->request->header('Authorization', '');
$token = $this->jwtService->extractTokenFromHeader($authorization);

if ($token) {
$this->jwtService->revokeToken($token);
}

Log::info('用户注销', [
'user_id' => $this->getUserId(),
'ip' => $this->request->ip()
]);

return $this->success([], '注销成功');
}

/**
* 记录登录尝试
* @param string $username 用户名
* @param bool $success 是否成功
* @param int $userId 用户ID
* @return void
*/
private function logLoginAttempt(string $username, bool $success, int $userId = 0): void
{
try {
Db::name('login_logs')->insert([
'username' => $username,
'user_id' => $userId,
'ip' => $this->request->ip(),
'user_agent' => $this->request->header('User-Agent'),
'success' => $success ? 1 : 0,
'created_at' => date('Y-m-d H:i:s')
]);
} catch (\Exception $e) {
Log::error('记录登录日志失败', ['error' => $e->getMessage()]);
}
}
}

部署与性能优化

Nginx配置优化

配置Nginx实现安全防护和性能优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
server {
listen 80;
server_name api.example.com;
root /var/www/html/public;
index index.php;

# 安全头设置
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

# CORS配置
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
add_header Access-Control-Max-Age 86400 always;

# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}

# 限制请求大小
client_max_body_size 10M;

# 限制请求频率
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;

# PHP处理
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;

# 安全设置
fastcgi_hide_header X-Powered-By;
fastcgi_read_timeout 300;
}

# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# 隐藏敏感文件
location ~ /\. {
deny all;
}

location ~ /(composer|package)\.json$ {
deny all;
}
}

性能监控配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// config/middleware.php
return [
// API应用中间件
'api' => [
// 性能监控中间件
\app\api\middleware\PerformanceMonitor::class,
// XSS防护中间件
\app\api\middleware\XssProtection::class,
// 请求限流中间件
\app\api\middleware\RateLimit::class,
],

// 需要JWT认证的路由
'auth' => [
\app\api\middleware\JwtAuth::class,
\app\api\middleware\CsrfProtection::class,
]
];

最佳实践与安全建议

1. API设计原则

  • RESTful设计:遵循REST架构风格
  • 版本控制:通过URL或Header进行API版本管理
  • 统一响应格式:保持响应数据结构一致性
  • 错误处理:提供详细的错误码和错误信息

2. 安全防护策略

  • 输入验证:严格验证所有输入数据
  • 输出编码:对输出数据进行适当编码
  • 访问控制:实现细粒度的权限控制
  • 日志监控:记录关键操作和异常情况

3. 性能优化建议

  • 缓存策略:合理使用Redis缓存热点数据
  • 数据库优化:优化查询语句和索引设计
  • 异步处理:使用队列处理耗时操作
  • CDN加速:静态资源使用CDN分发

4. 监控与运维

  • 性能监控:监控API响应时间和错误率
  • 安全监控:监控异常访问和攻击行为
  • 日志分析:定期分析日志发现潜在问题
  • 备份策略:建立完善的数据备份机制

总结

ThinkPHP8为API开发提供了强大的基础设施和安全机制。通过合理使用JWT认证、XSS/CSRF防护、输入验证等安全措施,可以构建安全可靠的API服务。同时,结合Nginx配置优化、缓存策略和性能监控,能够确保API服务的高性能和稳定性。

在实际项目中,需要根据具体业务需求调整安全策略和性能优化方案,建立完善的监控和运维体系,确保API服务的安全性、稳定性和可扩展性。掌握这些技术和最佳实践,能够显著提升ThinkPHP8 API开发的质量和效率。

本站由 提供部署服务