首页
直播
壁纸
友链
搜索
1
微信小程序支付全链路实战:JSAPI 下单、调起支付、回调验签与退款
266 阅读
2
微信小程序云开发实战:云函数、云数据库与云存储的正确使用姿势
256 阅读
3
微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式
255 阅读
4
微信小程序 Skyline 渲染引擎实战:worklet 动画从原理到落地
253 阅读
5
微信小程序分包进阶:独立分包、预下载与分包异步化实战
246 阅读
服务器运维
后端技术
前端技术
梯子
数据库
小程序
登录
搜索
标签搜索
fastadmin
Redis
微信小程序
前端开发
RabbitMQ
Go
服务器
codex
buildadmin
小程序
mysql
Nginx
Docker
Vue3
Node.js
MySQL优化
Linux
TypeScript
JWT
PHP
沿途的风景
累计撰写
74
篇文章
累计收到
0
条评论
首页
栏目
服务器运维
后端技术
前端技术
梯子
数据库
小程序
页面
直播
壁纸
友链
搜索到
74
篇与
» admin
的结果
2026-04-22
Redis 集群方案详解:主从复制、哨兵与 Cluster
前言当单机 Redis 无法满足业务需求时,就需要引入集群方案。Redis 提供了三种主要的集群模式:主从复制、哨兵模式和 Cluster 集群。本文将系统讲解三种方案的原理、搭建和适用场景。一、主从复制1.1 架构 ┌──────────────┐ │ Master │ │ (读+写) │ └──────┬───────┘ │ 复制 ┌──────────┼──────────┐ │ │ │ ┌──────▼───┐ ┌───▼────┐ ┌──▼─────┐ │ Slave 1 │ │Slave 2 │ │Slave 3 │ │ (只读) │ │(只读) │ │(只读) │ └──────────┘ └────────┘ └────────┘1.2 工作原理全量同步:Slave 首次连接 Master 时,Master 执行 BGSAVE 生成 RDB 文件,发送给 Slave增量同步:全量同步完成后,Master 将新的写命令实时同步给 Slave断线重连:Slave 断线后重连,通过 replid + offset 判断是否可以增量同步1.3 搭建配置# slave1.conf port 6380 # 配置主节点 slaveof 192.168.1.10 6379 # 从节点只读 slave-read-only yes # 主节点密码 masterauth your_password# 启动从节点 redis-server slave1.conf # 验证主从关系 redis-cli -p 6380 INFO replication # role:slave # master_host:192.168.1.10 # master_port:63791.4 优缺点优点缺点配置简单Master 宕机需手动切换读写分离,提升读性能不具备自动故障转移数据有副本全量同步时可能阻塞 Master二、哨兵模式(Sentinel)2.1 架构┌──────────┐ ┌──────────┐ ┌──────────┐ │Sentinel 1│ │Sentinel 2│ │Sentinel 3│ │ (监控) │ │ (监控) │ │ (监控) │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ └──────┬───────┴──────┬──────┘ │ │ ┌──────▼──────┐ ┌────▼─────┐ │ Master │ │ Slave │ │ (读+写) │ │ (只读) │ └─────────────┘ └──────────┘2.2 工作原理监控:Sentinel 不断向 Master/Slave 发送 PING 命令主观下线:某个 Sentinel 发现节点无响应,标记为主观下线客观下线:超过半数 Sentinel 都报告节点下线,标记为客观下线选举 Leader:Sentinel 之间选举一个 Leader 执行故障转移故障转移:Leader 选一个 Slave 提升为新 Master,通知其他 Slave 和客户端2.3 搭建配置# sentinel1.conf port 26379 # 监控的 Master(名称 IP 端口 仲裁人数) sentinel monitor mymaster 192.168.1.10 6379 2 # Master 无响应 30 秒判定下线 sentinel down-after-milliseconds mymaster 30000 # 故障转移超时时间 sentinel failover-timeout mymaster 180000 # 同时只允许1个Slave做同步 sentinel parallel-syncs mymaster 1 # Master 密码 sentinel auth-pass mymaster your_password# 启动3个Sentinel redis-sentinel sentinel1.conf redis-sentinel sentinel2.conf redis-sentinel sentinel3.conf2.4 故障转移过程1. Master宕机 │ 2. Sentinels检测到Master无响应 │ 3. 超过quorum(2)个Sentinel标记为客观下线 │ 4. 选举Leader Sentinel │ 5. Leader选择最优Slave提升为Master: - 排除故障Slave - 选择offset最大的Slave(数据最新) - 优先级高的Slave优先 │ 6. 其他Slave指向新Master │ 7. 通知客户端新Master地址 │ 8. 旧Master恢复后变为Slave2.5 客户端连接// PHP 客户端连接 Sentinel $sentinels = [ 'tcp://192.168.1.10:26379', 'tcp://192.168.1.11:26379', 'tcp://192.168.1.12:26379', ]; $redis = new RedisSentinel($sentinels, [ 'name' => 'mymaster', 'password' => 'your_password', ]); // 自动路由到当前 Master $redis->set('key', 'value');三、Cluster 集群3.1 架构┌─────────────────────────────────────────────────────┐ │ Redis Cluster │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Node A │ │ Node B │ │ Node C │ │ │ │Master │ │Master │ │Master │ │ │ │槽0-5460 │ │槽5461- │ │槽10923- │ │ │ │ │ │ 10922 │ │ 16383 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │ Node A' │ │ Node B' │ │ Node C' │ │ │ │ Slave │ │ Slave │ │ Slave │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────────────────┘3.2 核心概念哈希槽(Hash Slot)Redis Cluster 有 16384 个哈希槽,分布在所有 Master 节点上:# 计算key属于哪个槽 slot = CRC16(key) % 16384 # 示例 # key="user:1001" → CRC16 → slot=12345 # slot 12345 属于 Node C (10923-16383)节点通信(Gossip协议)每个节点定期向其他节点发送 PING 消息,交换集群状态信息。3.3 搭建 Cluster方式一:手动搭建(6节点,3主3从)# 创建6个配置文件 for port in 7001 7002 7003 7004 7005 7006; do mkdir -p /redis/cluster/$port cat > /redis/cluster/$port/redis.conf << EOF port $port cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes dir /redis/cluster/$port EOF done # 启动6个节点 for port in 7001 7002 7003 7004 7005 7006; do redis-server /redis/cluster/$port/redis.conf & done # 创建集群 redis-cli --cluster create \ 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \ 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \ --cluster-replicas 1方式二:Docker Compose 搭建version: '3.8' services: redis-node-1: image: redis:7 command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7001 ports: - "7001:7001" networks: - redis-cluster redis-node-2: image: redis:7 command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7002 ports: - "7002:7002" networks: - redis-cluster # ... node-3 到 node-6 同理 redis-cluster-init: image: redis:7 command: > bash -c "sleep 10 && echo yes | redis-cli --cluster create redis-node-1:7001 redis-node-2:7002 redis-node-3:7003 redis-node-4:7004 redis-node-5:7005 redis-node-6:7006 --cluster-replicas 1" depends_on: - redis-node-1 - redis-node-2 - redis-node-3 - redis-node-4 - redis-node-5 - redis-node-6 networks: - redis-cluster networks: redis-cluster: driver: bridge3.4 客户端使用// PHP 使用 Redis Cluster $redis = new RedisCluster( null, // null 表示自动发现节点 [ '127.0.0.1:7001', '127.0.0.1:7002', '127.0.0.1:7003', ], 2, // 连接超时 2, // 读写超时 true, // 持久化连接 'password' ); // 使用方式与单机一致,客户端自动路由 $redis->set('user:1001', 'Alice'); $redis->set('user:1002', 'Bob'); $redis->set('user:1003', 'Charlie'); // 不同的key可能落在不同节点 // user:1001 → slot 12345 → Node C // user:1002 → slot 6789 → Node B // user:1003 → slot 456 → Node A3.5 Hash Tag如果需要让多个 key 落在同一个槽,使用 Hash Tag:# 用 {} 指定hash计算的部分 SET {user:1001}:profile "Alice's profile" SET {user:1001}:settings '{"theme":"dark"}' SET {user:1001}:cart "[]" # 三个key都计算 user:1001 的CRC16,落在同一个槽 # 可以在同一节点执行批量操作 MGET {user:1001}:profile {user:1001}:settings {user:1001}:cart四、三种方案对比特性主从复制Sentinel 哨兵Cluster 集群数据分片否否是(16384槽)自动故障转移否是是读写分离是是是Master单点瓶颈是是否最少节点数23+Sentinel6复杂度低中高适用场景读多写少中小规模大规模五、选型建议数据量 < 10GB? ├── 是 → 单机Redis够用 │ └── 需要高可用? │ ├── 是 → Sentinel(3节点+3哨兵) │ └── 否 → 单机 │ └── 否 → 数据量大/并发高 └── Redis Cluster(3主3从起步)生产环境推荐中小项目:Sentinel 哨兵(1主2从+3哨兵)大型项目:Redis Cluster(3主3从起步,按需扩展)极大规模:Cluster + 多机房部署 + 代理层六、运维注意事项6.1 内存监控# 查看各节点内存使用 redis-cli -p 7001 INFO memory | grep used_memory_human redis-cli -p 7002 INFO memory | grep used_memory_human redis-cli -p 7003 INFO memory | grep used_memory_human # 集群整体状态 redis-cli -p 7001 CLUSTER INFO redis-cli -p 7001 CLUSTER NODES6.2 扩容(Cluster)# 新增节点 redis-server /redis/cluster/7007/redis.conf # 加入集群 redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001 # 分配槽位(从现有节点迁移部分槽到新节点) redis-cli --cluster reshard 127.0.0.1:7001 \ --cluster-from <现有节点ID> \ --cluster-to <新节点ID> \ --cluster-slots 1000 \ --cluster-yes6.3 数据迁移# 在线迁移(不停服) redis-cli --cluster rebalance 127.0.0.1:7001 \ --cluster-use-empty-masters \ --cluster-threshold 1总结方案核心价值关键限制主从复制读写分离、数据备份无自动故障转移Sentinel自动故障转移、高可用单Master写入瓶颈Cluster数据分片、水平扩展不支持跨槽事务、复杂度高选择原则:先评估数据量和并发量,小规模用 Sentinel,大规模用 Cluster,不要过度设计。
2026年04月22日
15 阅读
0 评论
0 点赞
2026-04-02
Redis 分布式锁实现:从 SET NX 到 Redlock 算法
前言分布式锁是分布式系统中控制资源访问的核心机制。Redis 因其单线程特性和高性能,是实现分布式锁的首选方案。本文将从最简单的实现出发,逐步演进到生产级的 Redlock 算法。一、为什么需要分布式锁1.1 单机锁的问题// 单机环境:PHP文件锁或flock可以解决 $fp = fopen('/tmp/lock.txt', 'w+'); flock($fp, LOCK_EX); // 加锁 // 执行业务逻辑 flock($fp, LOCK_UN); // 释放锁 fclose($fp);但在分布式环境中,请求可能被分配到不同服务器,文件锁无法跨机器生效。1.2 分布式锁的核心要求要求说明互斥性同一时刻只有一个客户端持有锁避免死锁锁必须有过期时间,防止持有者宕机导致永久锁死解铃还须系铃人只有锁的持有者才能释放锁高可用锁服务自身不能成为单点二、Redis 分布式锁演进2.1 版本一:SETNX(初版)// 最简单的实现 $lockKey = 'lock:order:1001'; // 尝试加锁 $redis->setnx($lockKey, 1); // 返回1表示成功,0表示失败 // 设置过期时间(防死锁) $redis->expire($lockKey, 30); // 业务逻辑 processOrder(); // 释放锁 $redis->del($lockKey);致命问题:setnx 和 expire 是两条命令,不保证原子性。如果 setnx 成功后服务崩溃,锁将永远不会过期,导致死锁。2.2 版本二:SET NX EX(推荐基础版)$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 生成唯一标识 // 一条命令完成加锁+过期,原子操作 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if ($result) { try { // 获取锁成功,执行业务 processOrder(); } finally { // 释放锁 $redis->del($lockKey); } } else { // 获取锁失败 throw new Exception('系统繁忙,请稍后重试'); }改进:SET key value NX EX 30 是原子操作,解决了死锁问题。遗留问题:释放锁时直接 DEL,可能删掉别人的锁。2.3 版本三:锁标识 + Lua 释放(安全版)$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 加锁 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if (!$result) { throw new Exception('获取锁失败'); } try { processOrder(); } finally { // 用Lua脚本安全释放锁:只有value匹配才删除 $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end "; $redis->eval($luaScript, [$lockKey, $lockValue], 1); }为什么需要 Lua 脚本?时间线: T1: 客户端A GET lock → value=A T2: 锁过期,自动删除 T3: 客户端B SET lock NX → 成功,value=B T4: 客户端A DEL lock → 删掉了B的锁!Lua 脚本保证了 GET + 判断 + DEL 三步原子执行。2.4 版本四:锁续期(生产可用版)业务执行时间可能超过锁的过期时间,需要后台续期:class RedisLock { private $redis; private $lockKey; private $lockValue; private $ttl = 30; private $renewTimer = null; public function lock($key, $ttl = 30) { $this->lockKey = $key; $this->lockValue = uniqid('', true); $this->ttl = $ttl; // 尝试加锁,最多重试3次 for ($i = 0; $i < 3; $i++) { $result = $this->redis->set( $this->lockKey, $this->lockValue, ['NX', 'EX' => $this->ttl] ); if ($result) { $this->startRenew(); return true; } usleep(100000); // 100ms后重试 } return false; } /** * 后台续期(守护进程/定时器) * 每隔 TTL/3 秒续期一次 */ private function startRenew() { $renewInterval = intval($this->ttl / 3); // 使用Swoole定时器(或PCNTL信号) if (function_exists('swoole_timer_tick')) { $this->renewTimer = swoole_timer_tick( $renewInterval * 1000, function() { $this->renew(); } ); } } private function renew() { $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end "; $this->redis->eval( $luaScript, [$this->lockKey, $this->lockValue, $this->ttl], 1 ); } public function unlock() { // 停止续期 if ($this->renewTimer) { swoole_timer_clear($this->renewTimer); } // 安全释放 $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end "; $this->redis->eval( $luaScript, [$this->lockKey, $this->lockValue], 1 ); } }2.5 版本五:可重入锁同一线程多次获取同一把锁不应死锁:class ReentrantLock { private $redis; /** * 加锁(可重入) * 使用Hash结构:key=锁名, field=客户端ID, value=重入次数 */ public function lock($lockKey, $clientId, $ttl = 30) { $luaScript = " -- 锁不存在或当前客户端已持有 if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then -- 重入次数+1 redis.call('hincrby', KEYS[1], ARGV[1], 1) -- 设置过期时间 redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end "; return $this->redis->eval( $luaScript, [$lockKey, $clientId, $ttl], 1 ) === 1; } /** * 释放锁(可重入) * 重入次数-1,减到0时删除锁 */ public function unlock($lockKey, $clientId) { $luaScript = " -- 不是锁的持有者 if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil end -- 重入次数-1 local count = redis.call('hincrby', KEYS[1], ARGV[1], -1) if count > 0 then -- 还有重入次数,续期 redis.call('expire', KEYS[1], 30) return count else -- 重入次数归零,删除锁 redis.call('del', KEYS[1]) return 0 end "; return $this->redis->eval( $luaScript, [$lockKey, $clientId], 1 ); } } // 使用 $lock = new ReentrantLock(); $clientId = 'client-' . getmypid(); $lock->lock('lock:order', $clientId); // count=1 $lock->lock('lock:order', $clientId); // count=2(可重入) $lock->unlock('lock:order', $clientId); // count=1 $lock->unlock('lock:order', $clientId); // count=0, 锁删除三、Redlock 算法3.1 为什么需要 Redlock单 Redis 实例的分布式锁有一个问题:如果 Master 宕机,锁数据还没同步到 Slave,新 Master 上没有锁信息,其他客户端可以加锁成功,导致互斥性被破坏。Redlock 由 Redis 作者提出,通过多个独立 Redis 实例投票解决此问题。3.2 算法流程客户端 Redis集群 │ │── 加锁请求 ──→ Node1 ──→ 成功 (1/5) │── 加锁请求 ──→ Node2 ──→ 成功 (2/5) │── 加锁请求 ──→ Node3 ──→ 成功 (3/5) ← 超过半数 │── 加锁请求 ──→ Node4 ──→ 超时 │── 加锁请求 ──→ Node5 ──→ 超时 │ │ 加锁耗时 = 50ms │ 锁TTL = 30s │ 实际有效时间 = 30s - 50ms = 29.95s │ │←── 加锁成功(3/5节点同意)─────步骤:获取当前时间 T1依次向 N 个 Redis 节点请求加锁(相同的 key、value、较短TTL)获取当前时间 T2,计算加锁耗时 = T2 - T1如果加锁成功节点数 ≥ ⌈N/2⌉ + 1 且加锁耗时 < TTL,则加锁成功实际锁有效时间 = TTL - 加锁耗时如果加锁失败,向所有节点发送释放锁请求3.3 PHP 实现class Redlock { private $servers = []; public function __construct(array $redisServers) { foreach ($redisServers as $server) { $redis = new Redis(); $redis->connect($server['host'], $server['port']); if (isset($server['password'])) { $redis->auth($server['password']); } $this->servers[] = $redis; } } public function lock($resource, $ttl = 10000) { $lockValue = bin2hex(random_bytes(16)); $successCount = 0; $startTime = microtime(true) * 1000; // 向所有节点请求加锁 foreach ($this->servers as $redis) { try { $result = $redis->set( $resource, $lockValue, ['NX', 'PX' => $ttl] ); if ($result) { $successCount++; } } catch (Exception $e) { // 节点不可用,继续尝试其他节点 } } $elapsed = microtime(true) * 1000 - $startTime; $quorum = floor(count($this->servers) / 2) + 1; // 判断是否加锁成功 if ($successCount >= $quorum && $elapsed < $ttl) { // 返回锁信息(包含实际有效时间) return [ 'key' => $resource, 'value' => $lockValue, 'validity_time' => $ttl - intval($elapsed), ]; } // 加锁失败,释放所有已加的锁 $this->unlock($resource, $lockValue); return false; } public function unlock($resource, $lockValue) { $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end "; foreach ($this->servers as $redis) { try { $redis->eval($luaScript, [$resource, $lockValue], 1); } catch (Exception $e) { // 忽略 } } } } // 使用 $redlock = new Redlock([ ['host' => '192.168.1.10', 'port' => 6379, 'password' => 'pass1'], ['host' => '192.168.1.11', 'port' => 6379, 'password' => 'pass2'], ['host' => '192.168.1.12', 'port' => 6379, 'password' => 'pass3'], ['host' => '192.168.1.13', 'port' => 6379, 'password' => 'pass4'], ['host' => '192.168.1.14', 'port' => 6379, 'password' => 'pass5'], ]); $lock = $redlock->lock('lock:order:1001', 10000); // TTL=10秒 if ($lock) { try { // 执行业务逻辑(注意在 validity_time 内完成) processOrder(); } finally { $redlock->unlock($lock['key'], $lock['value']); } } else { throw new Exception('获取分布式锁失败'); }3.4 Redlock 的争议Martin Kleppmann(《DDIA》作者)曾质疑 Redlock:时钟漂移可能导致锁失效GC 暂停可能导致客户端持有过期锁实际建议:如果对正确性要求极高(如金融场景),用 ZooKeeper 或 etcd如果是普通业务场景(如防重复下单),单 Redis + Lua 足够Redlock 适合需要比单 Redis 更高可靠性,但又不想引入 ZK 的场景四、实战:防重复下单class OrderService { private $redis; /** * 防重复下单 */ public function createOrder($userId, $productId, $quantity) { $lockKey = "lock:order:{$userId}:{$productId}"; $lockValue = uniqid('', true); // 加锁(10秒过期) $locked = $this->redis->set( $lockKey, $lockValue, ['NX', 'EX' => 10] ); if (!$locked) { throw new Exception('请勿重复提交,10秒后重试'); } try { // 1. 检查库存 $stock = $this->redis->get("stock:{$productId}"); if ($stock < $quantity) { throw new Exception('库存不足'); } // 2. 扣减库存 $this->redis->decrBy("stock:{$productId}", $quantity); // 3. 创建订单 $orderId = $this->db->insert('orders', [ 'user_id' => $userId, 'product_id' => $productId, 'quantity' => $quantity, 'status' => 'pending', 'created_at' => date('Y-m-d H:i:s'), ]); // 4. 发送延迟消息(30分钟未支付自动取消) $this->mq->publish('order_timeout', [ 'order_id' => $orderId, ], 1800); return $orderId; } catch (Exception $e) { // 回滚库存 $this->redis->incrBy("stock:{$productId}", $quantity); throw $e; } finally { // 安全释放锁 $lua = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; $this->redis->eval($lua, [$lockKey, $lockValue], 1); } } }五、各方案对比方案互斥性防死锁容错性性能复杂度SETNX+EXPIRE✅❌❌高低SET NX EX✅✅❌高低SET NX EX + Lua释放✅✅❌高中锁续期✅✅❌高中高可重入锁✅✅❌高高Redlock✅✅✅中高总结Redis 分布式锁的演进路线:SET NX EX 原子加锁 → 基础方案Lua 脚本安全释放 → 必须做后台续期 → 长任务必须做可重入 → 递归/嵌套场景需要Redlock → 对容错性要求高时考虑生产推荐:单 Redis + SET NX EX + Lua 释放 + 续期,覆盖 95% 场景。
2026年04月02日
18 阅读
0 评论
0 点赞
2026-03-29
FastAdmin API 接口开发:前后端分离实践
前言随着前后端分离架构的普及,FastAdmin 的 API 模块越来越重要。本文将系统讲解如何使用 FastAdmin 开发 RESTful API 接口,包括 Token 认证、参数验证、数据返回格式规范以及与前端框架的对接实践。一、API 模块概述FastAdmin 的 API 模块位于 application/api/,专门用于提供接口服务:application/api/ ├── controller/ │ ├── Common.php # 公共控制器 │ ├── Index.php # 首页接口 │ ├── User.php # 用户接口 │ ├── Api.php # 基类控制器 │ └── Demo.php # 示例接口 ├── model/ │ └── User.php # API 用户模型 └── config.php # API 模块配置二、API 基类控制器所有 API 控制器继承自 app\common\controller\Api:<?php // application/common/controller/Api.php namespace app\common\controller; use think\Controller; use app\common\library\Token; use app\common\library\Auth; class Api extends Controller { // 不需要登录的方法 protected $noNeedLogin = ['*']; // 不需要权限验证的方法 protected $noNeedRight = ['*']; // 当前请求的 Token protected $token = ''; // 当前登录用户 protected $auth = null; // 初始化 public function _initialize() { parent::_initialize(); // 跨域处理 $this->checkCors(); // Token 认证 $this->token = $this->request->server('HTTP_TOKEN', $this->request->request('token', '')); // 初始化认证 $this->auth = Auth::instance(); // 检查是否需要登录 $action = strtolower($this->request->action()); if (!in_array($action, array_map('strtolower', $this->noNeedLogin))) { if (!$this->auth->isLogin()) { $this->error(__('Please login first'), null, 401); } } } // 跨域设置 protected function checkCors() { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type, Authorization'); if ($this->request->method() == 'OPTIONS') { $this->success(); } } // 成功返回 protected function success($msg = '', $data = null, $code = 1) { $this->result($msg, $data, $code); } // 失败返回 protected function error($msg = '', $data = null, $code = 0) { $this->result($msg, $data, $code); } // 统一返回格式 protected function result($msg, $data = null, $code = 1) { $result = [ 'code' => $code, 'msg' => $msg, 'time' => time(), 'data' => $data, ]; throw new \think\exception\HttpResponseException( json($result) ); } }统一返回格式{ "code": 1, "msg": "获取成功", "time": 1721800000, "data": { "id": 1, "username": "admin" } }字段说明code1=成功,0=失败msg提示消息time服务器时间戳data业务数据三、用户认证3.1 注册接口<?php // application/api/controller/User.php namespace app\api\controller; use app\common\controller\Api; use app\common\model\User; use app\common\library\Token; use think\Validate; class User extends Api { protected $noNeedLogin = ['register', 'login']; protected $noNeedRight = ['*']; /** * 用户注册 * POST /api/user/register */ public function register() { $username = $this->request->post('username'); $password = $this->request->post('password'); $email = $this->request->post('email'); $mobile = $this->request->post('mobile'); // 参数验证 $validate = new Validate([ 'username' => 'require|length:3,30', 'password' => 'require|length:6,30', 'email' => 'require|email', 'mobile' => 'require|regex:/^1[3-9]\d{9}$/', ], [ 'username.require' => '用户名不能为空', 'username.length' => '用户名长度3-30个字符', 'password.require' => '密码不能为空', 'password.length' => '密码长度6-30个字符', 'email.email' => '邮箱格式不正确', 'mobile.regex' => '手机号格式不正确', ]); $data = [ 'username' => $username, 'password' => $password, 'email' => $email, 'mobile' => $mobile, ]; if (!$validate->check($data)) { $this->error($validate->getError()); } // 检查用户名是否已存在 if (User::where('username', $username)->find()) { $this->error('用户名已存在'); } // 创建用户 $user = new User(); $user->allowField(true)->save([ 'username' => $username, 'password' => \app\common\library\Hash::make($password), 'email' => $email, 'mobile' => $mobile, 'jointime' => time(), 'createtime' => time(), 'updatetime' => time(), ]); // 生成 Token $token = Token::create($user->id, 'user', 86400 * 7); $this->success('注册成功', [ 'token' => $token, 'user' => [ 'id' => $user->id, 'username' => $user->username, 'email' => $user->email, 'mobile' => $user->mobile, ], ]); } /** * 用户登录 * POST /api/user/login */ public function login() { $account = $this->request->post('account'); $password = $this->request->post('password'); if (!$account || !$password) { $this->error('账号和密码不能为空'); } // 支持用户名/邮箱/手机号登录 $user = User::where('username|email|mobile', $account)->find(); if (!$user) { $this->error('账号不存在'); } if (!\app\common\library\Hash::check($password, $user->password)) { $this->error('密码错误'); } if ($user->status != 'normal') { $this->error('账号已被禁用'); } // 更新登录信息 $user->save([ 'logintime' => time(), 'loginip' => $this->request->ip(), ]); // 生成 Token $token = Token::create($user->id, 'user', 86400 * 7); $this->success('登录成功', [ 'token' => $token, 'user' => [ 'id' => $user->id, 'username' => $user->username, 'avatar' => $user->avatar, 'email' => $user->email, ], ]); } /** * 获取当前用户信息 * GET /api/user/info */ public function info() { $user = $this->auth->getUser(); $this->success('获取成功', [ 'id' => $user->id, 'username' => $user->username, 'avatar' => $user->avatar, 'email' => $user->email, 'mobile' => $user->mobile, 'createtime'=> $user->createtime, ]); } /** * 退出登录 * POST /api/user/logout */ public function logout() { Token::clear($this->token); $this->success('退出成功'); } }3.2 Token 机制FastAdmin 的 Token 存储在 fa_token 表中:// 生成 Token(有效期 7 天) $token = Token::create($userId, 'user', 86400 * 7); // 验证 Token $userId = Token::get($token); // 刷新 Token $token = Token::refresh($token); // 清除 Token Token::clear($token);前端请求时在 Header 中携带 Token:// axios 请求拦截器 axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['token'] = token; } return config; }); // 响应拦截器处理 401 axios.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } return Promise.reject(error); } );四、RESTful 接口开发4.1 文章接口完整实现<?php // application/api/controller/Article.php namespace app\api\controller; use app\common\controller\Api; use app\common\model\Article as ArticleModel; use think\Validate; class Article extends Api { protected $noNeedLogin = ['index', 'detail']; protected $noNeedRight = ['*']; /** * 文章列表 * GET /api/article/index?page=1&limit=10&keyword=xxx */ public function index() { $page = $this->request->get('page/d', 1); $limit = $this->request->get('limit/d', 10); $keyword = $this->request->get('keyword', ''); $categoryId = $this->request->get('category_id/d', 0); $query = ArticleModel::where('status', 'normal'); // 关键词搜索 if ($keyword) { $query->where('title', 'like', "%{$keyword}%"); } // 分类筛选 if ($categoryId) { $query->where('category_id', $categoryId); } // 分页查询 $total = $query->count(); $list = $query->order('id', 'desc') ->field('id,title,author,views,createtime') ->page($page, $limit) ->select(); $this->success('获取成功', [ 'total' => $total, 'page' => $page, 'limit' => $limit, 'list' => $list, ]); } /** * 文章详情 * GET /api/article/detail?id=1 */ public function detail() { $id = $this->request->get('id/d', 0); if (!$id) { $this->error('参数错误'); } $article = ArticleModel::where('id', $id) ->where('status', 'normal') ->find(); if (!$article) { $this->error('文章不存在'); } // 浏览量 +1 ArticleModel::where('id', $id)->inc('views')->update(); $this->success('获取成功', $article); } /** * 创建文章 * POST /api/article/add */ public function add() { $data = $this->request->post(); $data['admin_id'] = $this->auth->id; $validate = new Validate([ 'title' => 'require|length:2,100', 'content' => 'require|min:10', ], [ 'title.require' => '标题不能为空', 'content.require' => '内容不能为空', ]); if (!$validate->check($data)) { $this->error($validate->getError()); } $data['createtime'] = time(); $data['updatetime'] = time(); $article = new ArticleModel(); $article->allowField(true)->save($data); $this->success('创建成功', ['id' => $article->id]); } /** * 编辑文章 * PUT /api/article/edit?id=1 */ public function edit() { $id = $this->request->get('id/d', 0); $data = $this->request->post(); $article = ArticleModel::find($id); if (!$article) { $this->error('文章不存在'); } // 权限检查:只能编辑自己的文章 if ($article->admin_id != $this->auth->id) { $this->error('无权操作', null, 403); } $data['updatetime'] = time(); $article->allowField(true)->save($data); $this->success('修改成功'); } /** * 删除文章 * DELETE /api/article/del?id=1 */ public function del() { $id = $this->request->get('id/d', 0); $article = ArticleModel::find($id); if (!$article) { $this->error('文章不存在'); } if ($article->admin_id != $this->auth->id) { $this->error('无权操作', null, 403); } $article->delete(); $this->success('删除成功'); } }4.2 路由配置在 application/api/route.php 中定义路由规则:<?php use think\Route; // 文章相关 Route::get('articles', 'api/article/index'); Route::get('article/:id', 'api/article/detail'); Route::post('article', 'api/article/add'); Route::put('article/:id', 'api/article/edit'); Route::delete('article/:id', 'api/article/del'); // 用户相关 Route::post('register', 'api/user/register'); Route::post('login', 'api/user/login'); Route::get('user/info', 'api/user/info'); Route::post('logout', 'api/user/logout');五、接口文档自动生成FastAdmin 内置 API 文档生成功能:# 生成 API 文档 php think api --force=true文档输出到 public/api.html,包含所有接口的参数说明、返回示例和在线测试功能。注释规范/** * @ApiTitle (文章列表) * @ApiSummary (获取文章列表,支持分页和搜索) * @ApiMethod (GET) * @ApiRoute (/api/article/index) * @ApiParams (name="page", type="integer", required=false, description="页码,默认1") * @ApiParams (name="limit", type="integer", required=false, description="每页条数,默认10") * @ApiParams (name="keyword", type="string", required=false, description="搜索关键词") * @ApiReturn ({"code":1,"msg":"获取成功","data":{"total":100,"list":[...]}}) */ public function index() { // ... }六、与前端对接6.1 Vue3 对接示例// api/article.ts import axios from 'axios' const API_BASE = '/api' export interface Article { id: number title: string author: string views: number createtime: number } export interface ArticleListResponse { total: number page: number limit: number list: Article[] } export const getArticles = (params: { page?: number limit?: number keyword?: string }) => { return axios.get<{ code: number; data: ArticleListResponse }>( `${API_BASE}/article/index`, { params } ) } export const getArticleDetail = (id: number) => { return axios.get<{ code: number; data: Article }>( `${API_BASE}/article/detail`, { params: { id } } ) }6.2 小程序对接示例// utils/request.js const BASE_URL = 'https://api.example.com' export function request(options) { const token = wx.getStorageSync('token') return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data, header: { 'token': token, 'Content-Type': 'application/json' }, success(res) { if (res.data.code === 1) { resolve(res.data.data) } else if (res.data.code === 0 && res.statusCode === 401) { wx.removeStorageSync('token') wx.redirectTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail(err) { reject(err) } }) }) } // 调用 request({ url: '/api/article/index', data: { page: 1, limit: 10 } }) .then(data => { console.log(data.list) })七、安全防护7.1 接口限流// 使用缓存实现简单限流 public function _initialize() { parent::_initialize(); $ip = $this->request->ip(); $key = 'api_rate_' . $ip; $count = \think\Cache::inc($key); if ($count == 1) { \think\Cache::expire($key, 60); // 60秒内 } if ($count > 60) { $this->error('请求过于频繁', null, 429); } }7.2 签名验证// 客户端生成签名 // sign = md5(参数按key排序拼接 + secret_key) public function verifySign($params, $sign) { unset($params['sign']); ksort($params); $str = http_build_query($params) . '&key=' . config('api_secret'); return md5($str) === $sign; }总结FastAdmin API 模块提供了完整的前后端分离方案:统一返回格式和错误处理Token 认证机制,支持多端登录RESTful 路由设计,接口语义清晰内置 API 文档自动生成配合 Vue3/小程序等前端框架,快速搭建完整应用
2026年03月29日
17 阅读
0 评论
0 点赞
2026-03-28
FastAdmin 插件开发实战:从零开发自定义插件
前言FastAdmin 的插件系统是其生态的核心——通过在线安装插件,可以快速扩展论坛、CMS、商城等功能模块,无需修改框架核心代码。本文将从零开始,带你完整开发一个 FastAdmin 插件。一、插件目录结构一个标准的 FastAdmin 插件目录如下:addons/ └── mydemo/ # 插件目录(插件标识) ├── Mydemo.php # 插件主类 ├── config.php # 插件配置文件 ├── info.ini # 插件信息文件 ├── controller/ │ └── Index.php # 插件控制器 ├── model/ │ └── Demo.php # 插件模型 ├── view/ │ └── index/ │ └── index.html # 插件视图 ├── public/ │ ├── css/ │ │ └── mydemo.css # 插件样式 │ └── js/ │ └── mydemo.js # 插件脚本 └── hooks.php # 钩子定义(可选)二、创建插件2.1 使用命令行创建php think addon -a mydemo -c create这会在 addons/mydemo/ 目录下自动生成插件骨架文件。2.2 info.ini 插件信息name = mydemo title = 示例插件 description = 这是一个 FastAdmin 示例插件 author = Your Name version = 1.0.0 state = 1字段说明name插件标识(目录名,全小写)title插件名称description插件描述author作者version版本号state状态:1=正常,0=禁用2.3 插件主类 Mydemo.php<?php namespace addons\mydemo; use think\Addons; class Mydemo extends Addons { // 插件安装时执行 public function install() { // 创建数据表 $sql = "CREATE TABLE IF NOT EXISTS `fa_mydemo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL DEFAULT '', `content` text, `createtime` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; \think\Db::execute($sql); return true; } // 插件卸载时执行 public function uninstall() { // 删除数据表 \think\Db::execute("DROP TABLE IF EXISTS `fa_mydemo`"); return true; } // 插件启用时执行 public function enable() { return true; } // 插件禁用时执行 public function disable() { return true; } // 钩子:在后台首页显示 public function adminIndexTop($params) { echo '<div class="alert alert-info">欢迎使用 Mydemo 插件!</div>'; } }2.4 config.php 插件配置<?php return [ [ 'name' => 'title', 'title' => '插件标题', 'type' => 'text', 'content' => '', 'value' => 'Mydemo Plugin', 'rule' => 'required', 'msg' => '请填写插件标题', 'tip' => '显示在插件管理页面的标题', ], [ 'name' => 'display_count', 'title' => '显示数量', 'type' => 'number', 'content' => '', 'value' => '10', 'rule' => 'required', 'msg' => '请填写显示数量', 'tip' => '前台显示的数据条数', ], [ 'name' => 'is_open', 'title' => '是否开启', 'type' => 'radio', 'content' => ['0' => '关闭', '1' => '开启'], 'value' => '1', 'rule' => 'required', 'msg' => '', 'tip' => '', ], ];配置在后台 → 插件管理 → 配置 中可编辑,读取方式:$config = get_addon_config('mydemo'); $title = $config['title'];三、插件控制器3.1 后台控制器<?php // addons/mydemo/controller/Index.php namespace addons\mydemo\controller; use app\common\controller\Backend; use think\Db; class Index extends Backend { // 不需要登录的方法 protected $noNeedLogin = []; // 不需要权限验证的方法 protected $noNeedRight = []; /** * 插件后台首页 */ public function index() { $list = Db::name('mydemo')->order('id', 'desc')->paginate(15); $this->view->assign('list', $list); return $this->view->fetch(); } /** * 添加数据 */ public function add() { if ($this->request->isPost()) { $data = $this->request->post('row/a'); $data['createtime'] = time(); Db::name('mydemo')->insert($data); $this->success('添加成功'); } return $this->view->fetch(); } /** * 删除数据 */ public function del($ids = null) { Db::name('mydemo')->where('id', 'in', $ids)->delete(); $this->success('删除成功'); } }3.2 访问路由插件控制器的 URL 格式为:http://yourdomain.com/addons/mydemo/index/index对应:addons - 固定前缀mydemo - 插件标识index - 控制器名index - 方法名3.3 前台控制器<?php // addons/mydemo/controller/Api.php namespace addons\mydemo\controller; use app\common\controller\Api; class Api extends Api { // 不需要登录的方法 protected $noNeedLogin = ['getList']; /** * 获取数据列表(API 接口) */ public function getList() { $count = \think\Db::name('mydemo')->count(); $list = \think\Db::name('mydemo') ->order('id', 'desc') ->limit(10) ->select(); $this->success('获取成功', [ 'total' => $count, 'list' => $list, ]); } }四、钩子机制(Hook)4.1 注册钩子在插件主类中定义钩子方法,方法名即钩子名:// addons/mydemo/Mydemo.php class Mydemo extends Addons { // 钩子:文章详情页底部 public function articleDetailBottom($params) { // $params 是传递的参数 $article = $params['article']; // 输出自定义内容 echo '<div class="mydemo-box">'; echo '<h3>相关推荐</h3>'; echo '</div>'; } // 钩子:后台导航栏 public function adminSidebar($params) { echo '<li><a href="' . addon_url('mydemo/index/index') . '">'; echo '<i class="fa fa-puzzle-piece"></i> <span>Mydemo</span>'; echo '</a></li>'; } }4.2 触发钩子在任意位置触发钩子:// 传递参数给钩子 \think\Hook::listen('article_detail_bottom', ['article' => $article]);4.3 FastAdmin 内置常用钩子钩子名触发位置参数admin_index_top后台首页顶部无admin_sidebar后台左侧菜单无article_detail_bottom文章详情底部article 数据user_register_after用户注册后user 数据config_init配置初始化配置数组五、插件打包发布5.1 打包# 将插件目录打包为 zip cd addons zip -r mydemo.zip mydemo/注意:打包时不要包含 runtime、temp 等临时目录。5.2 发布到插件市场注册 FastAdmin 官网账号进入"开发者中心"点击"发布插件"上传 zip 包,填写插件信息等待审核通过5.3 版本更新更新 info.ini 中的版本号后重新打包上传,用户可在后台一键升级。六、调试技巧6.1 开启插件调试// application/config.php 'app_debug' => true; // 确保插件目录有读写权限 chmod -R 755 addons/mydemo/6.2 查看插件日志// 在插件代码中记录日志 \think\Log::info('Mydemo: ' . json_encode($data)); // 查看日志 // runtime/log/202607/24.log6.3 热更新开发阶段修改插件代码后,清除缓存即可生效:php think clear --type=cache php think clear --type=temp七、完整插件开发流程1. php think addon -a mydemo -c create → 创建骨架 2. 编辑 info.ini → 填写插件信息 3. 编辑 Mydemo.php → 实现安装/卸载/钩子 4. 编辑 config.php → 定义插件配置项 5. 编写 controller/ → 实现业务逻辑 6. 编写 view/ → 编写视图模板 7. 编写 public/ → 添加前端资源 8. 后台安装插件 → 测试 → 调试 9. 打包 zip → 发布总结FastAdmin 插件系统通过目录约定 + 钩子机制实现了松耦合的功能扩展。核心要点:插件主类继承 think\Addons,实现 install/uninstall 生命周期通过 config.php 定义可视化配置项通过 Hook::listen() 在框架任意位置触发钩子插件可以有自己的 MVC 结构,独立于主应用
2026年03月28日
36 阅读
0 评论
0 点赞
2026-03-26
FastAdmin 权限认证体系:RBAC 角色与菜单管理
前言FastAdmin 内置了完整的 RBAC(基于角色的访问控制)权限体系,支持细粒度的菜单管理、操作权限和数据权限控制。本文将深入讲解 FastAdmin 权限认证的工作原理和实际配置方法。一、RBAC 权限模型概述FastAdmin 的权限体系包含三个核心实体:管理员(Admin)──→ 角色组(Group)──→ 权限规则(Rule)实体数据表说明管理员fa_admin后台用户,每个用户属于一个或多个角色组角色组fa_group角色定义,每个角色关联一组权限规则权限规则fa_auth_rule权限节点,对应控制器的每个操作方法二、权限认证流程2.1 认证类 AuthFastAdmin 的权限认证由 application/common/library/Auth.php 实现:namespace app\common\library; use think\Session; use think\Cookie; use app\admin\model\Admin as AdminModel; class Auth { protected $logined = false; // 是否登录 protected $admin = null; // 当前管理员信息 protected $error = ''; // 错误信息 protected $requestUri = ''; // 当前请求 URL // 检查是否已登录 public function isLogin() { if ($this->logined) { return true; } $admin = Session::get('admin'); if (!$admin) { return false; } // 验证管理员状态 $this->admin = AdminModel::get($admin['id']); if (!$this->admin || $this->admin['status'] != 'normal') { return false; } $this->logined = true; return true; } // 检查权限 public function check($name = '') { if (!$this->isLogin()) { return false; } // 超级管理员直接通过 if ($this->admin['id'] == 1) { return true; } // 获取当前用户的所有权限规则 $rules = $this->getRuleList(); if (!$name) { $name = strtolower($this->request->module() . '/' . $this->request->controller() . '/' . $this->request->action()); } $name = strtolower($name); // 检查是否有匹配的权限 foreach ($rules as $rule) { if ($this->checkRule($rule, $name)) { return true; } } return false; } }2.2 权限检查链路请求进入 → Backend 控制器初始化 → Auth::check() 检查当前 URL 是否有权限 → 无权限 → 返回 403 / 跳转登录 → 有权限 → 继续执行2.3 控制器中的权限注解FastAdmin 通过注解方式声明权限节点:// application/admin/controller/content/Article.php namespace app\admin\controller\content; use app\common\controller\Backend; class Article extends Backend { // 默认不需要登录的方法 protected $noNeedLogin = []; // 默认不需要权限验证的方法 protected $noNeedRight = []; /** * 文章列表 */ public function index() { return parent::index(); } /** * 添加文章 */ public function add() { return parent::add(); } /** * 编辑文章 */ public function edit($ids = null) { return parent::edit($ids); } /** * 删除文章 */ public function del($ids = null) { return parent::del($ids); } }每个 public 方法自动注册为一个权限节点,格式为 content/article/index。三、菜单管理3.1 菜单结构菜单存储在 fa_auth_rule 表中,采用树形结构:-- 查看菜单结构 SELECT id, pid, name, title, type FROM fa_auth_rule ORDER BY weigh DESC;字段说明id节点 IDpid父节点 ID(0 为顶级)name权限规则名(URL 路径)title菜单显示名称type类型:menu=菜单,file=操作节点icon菜单图标(FontAwesome)ismenu是否在左侧菜单显示(1=显示)3.2 命令行生成菜单# 根据控制器自动生成菜单 php think menu -c content/article # 强制覆盖 php think menu -c content/article -f 13.3 后台手动管理菜单进入 后台 → 权限管理 → 菜单规则,可以:添加/编辑/删除菜单节点拖拽排序设置菜单图标控制菜单是否显示3.4 菜单与权限的关系菜单规则(fa_auth_rule) ├── 内容管理 (ismenu=1, type=menu) │ ├── 文章管理 (ismenu=1, type=menu) │ │ ├── 查看 (ismenu=0, type=file) → content/article/index │ │ ├── 添加 (ismenu=0, type=file) → content/article/add │ │ ├── 编辑 (ismenu=0, type=file) → content/article/edit │ │ └── 删除 (ismenu=0, type=file) → content/article/delismenu=1 的节点显示在左侧导航栏,ismenu=0 的是操作按钮权限。四、角色组管理4.1 创建角色组进入 后台 → 权限管理 → 角色组:点击"添加"填写角色名称(如"内容编辑")勾选该角色拥有的权限保存4.2 角色权限继承角色组支持树形结构,子角色自动继承父角色的所有权限:超级管理员(id=1,拥有所有权限) ├── 内容主管(拥有内容管理全部权限) │ ├── 内容编辑(拥有文章增改权限,无删除权限) │ └── 内容审核(拥有文章审核权限) └── 系统管理员(拥有系统设置权限)4.3 数据权限FastAdmin 支持基于角色组的数据隔离:// 在控制器中启用数据权限 public function index() { // 只能查看自己创建的数据 $this->dataLimit = 'personal'; // 只能查看本角色组的数据 $this->dataLimit = 'auth'; // 自定义数据限制字段 $this->dataLimitField = 'admin_id'; return parent::index(); }数据权限模式说明:模式说明null不限制,可查看所有数据personal只能查看自己创建的数据auth只能查看本角色组及子角色组的数据五、API Token 认证除了后台 Session 认证,FastAdmin 的 API 模块支持 Token 认证:5.1 Token 生成// application/api/controller/User.php namespace app\api\controller; use app\common\controller\Api; use app\common\library\Token; class User extends Api { // 登录获取 Token public function login() { $username = $this->request->post('username'); $password = $this->request->post('password'); $admin = \app\admin\model\Admin::get(['username' => $username]); if (!$admin || $admin->password != md5(md5($password) . $admin->salt)) { $this->error('用户名或密码错误'); } // 生成 Token $token = Token::create($admin['id'], 'admin', 86400 * 7); $this->success('登录成功', [ 'token' => $token, 'admin' => $admin, ]); } }5.2 Token 验证中间件// API 请求头携带 Token // Authorization: Bearer xxx-xxx-xxx // 在 API 控制器中验证 namespace app\api\controller; class Order extends Api { // 需要登录的方法 protected $noNeedLogin = []; public function list() { // $this->auth 已自动初始化 $adminId = $this->auth->id; $orders = model('Order')->where('admin_id', $adminId)->select(); $this->success('获取成功', $orders); } }六、安全加固建议6.1 密码安全// FastAdmin 使用双重 MD5 加盐 // password = md5(md5(明文密码) . salt) // 自定义更安全的加密方式 \app\admin\model\Admin::event('before_insert', function ($admin) { $admin->salt = \fast\Random::alnum(8); $admin->password = password_hash($admin->password, PASSWORD_BCRYPT); });6.2 登录失败限制FastAdmin 默认 5 次登录失败后锁定 15 分钟,可在配置中调整:// application/extra/site.php 'login_failure_retry' => 5, // 最大失败次数 'login_failure_lock' => 900, // 锁定时间(秒)6.3 操作日志所有后台操作自动记录到 fa_admin_log 表:// 手动记录日志 \app\admin\model\AdminLog::record([ 'title' => '手动操作文章', 'content' => '修改了文章ID:' . $id, ]);总结FastAdmin 的权限体系基于标准 RBAC 模型,通过菜单规则表实现细粒度权限控制。核心要点:权限规则对应控制器的每个方法,自动注册角色组支持树形继承,子角色继承父角色权限数据权限支持 personal 和 auth 两种模式API 模块通过 Token 实现无状态认证
2026年03月26日
7 阅读
0 评论
0 点赞
1
...
8
9
10
...
15
0:00