帮助文档
{{userInfo.nickname}}
用户设置 退出登录

{{wikiTitle}}

鉴权说明

CRMEB多店系统 - 添加鉴权说明

一、概述

CRMEB多店系统采用基于JWT (JSON Web Token) 的身份认证机制,结合RBAC (基于角色的访问控制) 实现完整的权限管理。本文档详细介绍系统的鉴权机制实现和二次开发方法。

二、认证架构

2.1 认证流程图

用户登录 → 验证账号密码 → 生成JWT Token → 存储Token到缓存
    ↓
请求携带Token → 中间件验证 → 解析Token → 验证权限 → 执行业务

2.2 核心组件

组件 位置 功能
JwtAuth crmeb/utils/JwtAuth.php JWT Token生成和解析
CacheService crmeb/services/CacheService.php Token缓存管理
AuthTokenMiddleware app/http/middleware/ 登录验证中间件
RoleMiddleware app/http/middleware/ 权限验证中间件
AuthServices app/services/ 认证业务服务

2.3 各端认证体系

认证中间件 权限中间件 Token前缀
平台后台 AdminAuthTokenMiddleware AdminCkeckRoleMiddleware admin_
用户端 AuthTokenMiddleware - user_
门店端 AuthTokenMiddleware StoreCkeckRoleMiddleware store_
供应商端 AuthTokenMiddleware SupplierCkeckRoleMiddleware supplier_
收银端 AuthTokenMiddleware CashierCkeckRoleMiddleware cashier_

三、JWT Token机制

3.1 Token生成

核心服务类: crmeb/utils/JwtAuth.php

<?php
namespace crmeb\utils;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JwtAuth
{
    /**
     * Token过期时间(秒)
     */
    protected $exp = 86400;

    /**
     * 生成Token
     * @param int $id 用户ID
     * @param string $type 用户类型
     * @param string $auth 认证标识(通常是密码的MD5)
     * @return string
     */
    public function createToken($id, $type, $auth = '')
    {
        $host = app()->request->host();
        $time = time();

        $payload = [
            'iss' => $host,              // 签发者
            'aud' => $host,              // 接收者
            'iat' => $time,              // 签发时间
            'nbf' => $time,              // 生效时间
            'exp' => $time + $this->exp, // 过期时间
            'jti' => compact('id', 'type', 'auth')  // 自定义数据
        ];

        return JWT::encode($payload, $this->getSecretKey(), 'HS256');
    }

    /**
     * 解析Token
     * @param string $token
     * @return array [id, type, auth]
     */
    public function parseToken($token)
    {
        JWT::$leeway = 60;
        $this->token = JWT::decode($token, new Key($this->getSecretKey(), 'HS256'));

        $jti = $this->token->jti;
        return [$jti->id, $jti->type, $jti->auth ?? ''];
    }

    /**
     * 验证Token是否过期
     */
    public function verifyToken()
    {
        return $this->token->exp > time();
    }

    /**
     * 获取密钥
     */
    protected function getSecretKey()
    {
        return md5(env('JWT_SECRET', 'crmeb') . '@' . app()->request->host());
    }
}

3.2 Token缓存管理

核心服务类: crmeb/services/CacheService.php

<?php
namespace crmeb\services;

class CacheService
{
    /**
     * Token桶前缀
     */
    const TOKEN_BUCKET = 'token_bucket_';

    /**
     * 设置Token桶
     * @param string $key Token的MD5值
     * @param array $data Token数据
     * @param int $exp 过期时间(秒)
     */
    public static function setTokenBucket($key, $data, $exp = 86400)
    {
        return self::redisHandler()->set(self::TOKEN_BUCKET . $key, $data, $exp);
    }

    /**
     * 获取Token桶
     * @param string $key
     * @return mixed
     */
    public static function getTokenBucket($key)
    {
        return self::redisHandler()->get(self::TOKEN_BUCKET . $key);
    }

    /**
     * 检查Token是否存在
     * @param string $key
     * @return bool
     */
    public static function hasToken($key)
    {
        return self::redisHandler()->has(self::TOKEN_BUCKET . $key);
    }

    /**
     * 清除Token
     * @param string $key
     */
    public static function clearToken($key)
    {
        return self::redisHandler()->delete(self::TOKEN_BUCKET . $key);
    }
}

四、登录认证实现

4.1 平台后台登录

认证服务: app/services/system/admin/AdminAuthServices.php

<?php
namespace app\services\system\admin;

class AdminAuthServices extends BaseServices
{
    /**
     * 管理员登录
     * @param string $account 账号
     * @param string $password 密码
     * @return array
     */
    public function login(string $account, string $password)
    {
        // 1. 验证账号是否存在
        $adminInfo = $this->dao->getOne(['account' => $account, 'is_del' => 0]);
        if (!$adminInfo) {
            throw new ValidateException('账号不存在');
        }

        // 2. 验证账号状态
        if (!$adminInfo['status']) {
            throw new ValidateException('账号已被禁用');
        }

        // 3. 验证密码
        if (!password_verify($password, $adminInfo['pwd'])) {
            throw new ValidateException('密码错误');
        }

        // 4. 生成Token
        $tokenInfo = $this->createToken($adminInfo);

        // 5. 记录登录日志
        $this->updateLoginInfo($adminInfo['id']);

        return [
            'token' => $tokenInfo['token'],
            'exp' => $tokenInfo['exp'],
            'admin' => $adminInfo->hidden(['pwd'])->toArray()
        ];
    }

    /**
     * 创建Token
     * @param object $adminInfo
     * @return array
     */
    protected function createToken($adminInfo)
    {
        /** @var JwtAuth $jwtAuth */
        $jwtAuth = app()->make(JwtAuth::class);

        // 生成Token
        $token = $jwtAuth->createToken(
            $adminInfo['id'],
            'admin',
            md5($adminInfo['pwd'])  // 用于密码变更后Token失效
        );

        $exp = Config::get('admin.token_exp', 86400);

        // 存入缓存
        CacheService::setTokenBucket(md5($token), [
            'uid' => $adminInfo['id'],
            'type' => 'admin',
            'exp' => time() + $exp,
            'invalidNum' => 0  // 无效次数
        ], $exp);

        return compact('token', 'exp');
    }

    /**
     * 解析Token验证登录状态
     * @param string $token
     * @return array
     */
    public function parseToken(string $token): array
    {
        if (!$token || $token === 'undefined') {
            throw new AuthException(ApiErrorCode::ERR_LOGIN);
        }

        /** @var JwtAuth $jwtAuth */
        $jwtAuth = app()->make(JwtAuth::class);

        // 解析Token
        [$id, $type, $auth] = $jwtAuth->parseToken($token);

        // 检测缓存中Token是否存在
        $md5Token = md5($token);
        $cacheToken = CacheService::getTokenBucket($md5Token);
        if (!$cacheToken) {
            throw new AuthException(ApiErrorCode::ERR_LOGIN);
        }

        // 验证Token有效性
        try {
            $jwtAuth->verifyToken();
        } catch (\Throwable $e) {
            CacheService::clearToken($md5Token);
            throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID);
        }

        // 获取管理员信息
        $adminInfo = $this->dao->get($id);
        if (!$adminInfo) {
            throw new AuthException(ApiErrorCode::ERR_LOGIN_STATUS);
        }

        // 验证密码是否变更(密码变更后Token失效)
        if ($auth !== md5($adminInfo['pwd'])) {
            throw new AuthException(ApiErrorCode::ERR_LOGIN_STATUS);
        }

        return $adminInfo->hidden(['pwd'])->toArray();
    }
}

4.2 用户端登录

认证服务: app/services/user/UserAuthServices.php

<?php
namespace app\services\user;

class UserAuthServices extends BaseServices
{
    /**
     * 用户登录
     * @param string $account
     * @param string $password
     * @return array
     */
    public function login(string $account, string $password)
    {
        /** @var UserServices $userServices */
        $userServices = app()->make(UserServices::class);

        // 查找用户
        $user = $userServices->getOne(['account' => $account]);
        if (!$user) {
            throw new ValidateException('用户不存在');
        }

        // 验证密码
        if (!password_verify($password, $user['pwd'])) {
            throw new ValidateException('密码错误');
        }

        // 检查状态
        if (!$user['status']) {
            throw new ValidateException('账号已被禁用');
        }

        // 生成Token
        return $this->createToken($user);
    }

    /**
     * 手机号快捷登录
     * @param string $phone
     * @param string $code
     * @return array
     */
    public function mobileLogin(string $phone, string $code)
    {
        // 验证短信验证码
        $this->checkSmsCode($phone, $code);

        /** @var UserServices $userServices */
        $userServices = app()->make(UserServices::class);

        // 查找或创建用户
        $user = $userServices->getOne(['phone' => $phone]);
        if (!$user) {
            // 注册新用户
            $user = $userServices->register([
                'phone' => $phone,
                'account' => $phone
            ]);
        }

        return $this->createToken($user);
    }

    /**
     * 微信登录
     * @param array $wechatInfo
     * @return array
     */
    public function wechatLogin(array $wechatInfo)
    {
        /** @var WechatUserServices $wechatUserServices */
        $wechatUserServices = app()->make(WechatUserServices::class);

        // 通过openid查找或创建用户
        $user = $wechatUserServices->getOrCreateUser($wechatInfo);

        return $this->createToken($user);
    }

    /**
     * 创建用户Token
     * @param object $user
     * @return array
     */
    protected function createToken($user)
    {
        /** @var JwtAuth $jwtAuth */
        $jwtAuth = app()->make(JwtAuth::class);

        $token = $jwtAuth->createToken(
            $user['uid'],
            'user',
            md5($user['pwd'] ?? '')
        );

        $exp = 2592000;  // 30天

        CacheService::setTokenBucket(md5($token), [
            'uid' => $user['uid'],
            'type' => 'user',
            'exp' => time() + $exp
        ], $exp);

        return [
            'token' => $token,
            'exp' => $exp,
            'user' => $user->hidden(['pwd'])->toArray()
        ];
    }
}

五、认证中间件

5.1 平台后台认证中间件

<?php
// app/http/middleware/admin/AdminAuthTokenMiddleware.php

namespace app\http\middleware\admin;

use app\Request;
use app\services\system\admin\AdminAuthServices;
use crmeb\interfaces\MiddlewareInterface;
use think\facade\Config;

class AdminAuthTokenMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        // 获取Token(从Header中)
        $token = trim(ltrim(
            $request->header(Config::get('cookie.token_name', 'Authori-zation')), 
            'Bearer'
        ));

        /** @var AdminAuthServices $service */
        $service = app()->make(AdminAuthServices::class);

        // 解析验证Token
        $adminInfo = $service->parseToken($token);

        // 注入管理员信息到请求对象
        $request->isAdminLogin = !is_null($adminInfo);
        $request->adminId = (int)$adminInfo['id'];
        $request->adminInfo = $adminInfo;
        $request->adminType = $adminInfo['admin_type'] ?? 0;

        return $next($request);
    }
}

5.2 用户端认证中间件

<?php
// app/http/middleware/api/AuthTokenMiddleware.php

namespace app\http\middleware\api;

use app\Request;
use app\services\user\UserAuthServices;
use crmeb\exceptions\AuthException;
use crmeb\interfaces\MiddlewareInterface;

class AuthTokenMiddleware implements MiddlewareInterface
{
    /**
     * @param Request $request
     * @param \Closure $next
     * @param bool $force 是否强制登录
     */
    public function handle(Request $request, \Closure $next, bool $force = true)
    {
        $authInfo = null;
        $token = trim(ltrim($request->header('Authori-zation'), 'Bearer'));

        try {
            /** @var UserAuthServices $service */
            $service = app()->make(UserAuthServices::class);
            $authInfo = $service->parseToken($token);
        } catch (AuthException $e) {
            // 强制登录模式下,抛出异常
            if ($force) {
                return app('json')->make($e->getCode(), $e->getMessage());
            }
            // 非强制模式,继续执行(用户信息为空)
        }

        // 注入用户信息
        if (!is_null($authInfo)) {
            $request->user = function (string $key = null) use (&$authInfo) {
                if ($key) {
                    return $authInfo['user'][$key] ?? '';
                }
                return $authInfo['user'];
            };
            $request->tokenData = $authInfo['tokenData'];
        }

        $request->isLogin = !is_null($authInfo);
        $request->uid = is_null($authInfo) ? 0 : (int)$authInfo['user']->uid;

        return $next($request);
    }
}

六、权限验证(RBAC)

6.1 权限表结构

-- 菜单权限表
CREATE TABLE `eb_system_menus` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pid` int(11) NOT NULL DEFAULT '0' COMMENT '父级ID',
  `menu_name` varchar(100) NOT NULL COMMENT '菜单名称',
  `api_url` varchar(255) DEFAULT NULL COMMENT 'API接口地址',
  `methods` varchar(20) DEFAULT 'GET' COMMENT '请求方法',
  `auth_type` tinyint(1) DEFAULT '1' COMMENT '权限类型:1菜单 2接口',
  `type` tinyint(1) DEFAULT '1' COMMENT '类型:1平台 2门店 3供应商',
  PRIMARY KEY (`id`)
);

-- 角色表
CREATE TABLE `eb_system_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  `rules` text COMMENT '权限ID集合',
  `level` tinyint(1) DEFAULT '0' COMMENT '等级:0超级管理员',
  `type` tinyint(1) DEFAULT '1' COMMENT '类型',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态',
  PRIMARY KEY (`id`)
);

-- 管理员表
CREATE TABLE `eb_system_admin` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(50) NOT NULL COMMENT '账号',
  `pwd` varchar(255) NOT NULL COMMENT '密码',
  `roles` varchar(255) DEFAULT '' COMMENT '角色ID',
  `level` tinyint(1) DEFAULT '1' COMMENT '等级',
  PRIMARY KEY (`id`)
);

6.2 权限验证中间件

<?php
// app/http/middleware/admin/AdminCkeckRoleMiddleware.php

namespace app\http\middleware\admin;

use app\Request;
use app\services\system\SystemRoleServices;
use crmeb\exceptions\AuthException;
use crmeb\interfaces\MiddlewareInterface;
use crmeb\utils\ApiErrorCode;

class AdminCkeckRoleMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next)
    {
        // 检查是否已登录
        if (!$request->adminId() || !$request->adminInfo()) {
            throw new AuthException(ApiErrorCode::ERR_ADMINID_VOID);
        }

        // level为0是超级管理员,不验证权限
        if ($request->adminInfo()['level']) {
            /** @var SystemRoleServices $systemRoleService */
            $systemRoleService = app()->make(SystemRoleServices::class);
            $systemRoleService->verifiAuth($request);
        }

        return $next($request);
    }
}

6.3 权限验证服务

<?php
// app/services/system/SystemRoleServices.php

namespace app\services\system;

class SystemRoleServices extends BaseServices
{
    /**
     * 验证访问权限
     * @param Request $request
     */
    public function verifiAuth(Request $request)
    {
        // 获取当前访问的路由规则
        $rule = str_replace('adminapi/', '', trim(strtolower($request->rule()->getRule())));

        // 白名单路由不验证
        if (in_array($rule, ['setting/admin/logout', 'menuslist'])) {
            return true;
        }

        $method = trim(strtolower($request->method()));

        // 获取所有权限列表
        $allAuth = $this->getAllRoles(2);

        // 验证访问的接口是否在系统中定义
        $authList = array_map(function ($item) {
            return trim(strtolower($item['methods'])) . '@@' . trim(strtolower($item['api_url']));
        }, $allAuth);

        if (!in_array($method . '@@' . $rule, $authList)) {
            return true;  // 未定义的接口不限制
        }

        // 获取当前用户的权限列表
        $userAuth = $this->getRolesByAuth($request->adminInfo()['roles'], 2);

        // 验证是否有访问权限
        $hasAuth = array_filter($userAuth, function ($item) use ($rule, $method) {
            return trim(strtolower($item['api_url'])) === $rule 
                && $method === trim(strtolower($item['methods']));
        });

        if (empty($hasAuth)) {
            throw new AuthException(ApiErrorCode::ERR_AUTH);
        }

        return true;
    }

    /**
     * 根据角色获取权限
     * @param array $roles 角色ID数组
     * @return array
     */
    public function getRolesByAuth(array $roles, int $authType = 1)
    {
        if (empty($roles)) return [];

        // 获取角色的权限ID
        $ruleIds = $this->getRoleIds($roles);

        /** @var SystemMenusServices $menusService */
        $menusService = app()->make(SystemMenusServices::class);

        return $menusService->getColumn([
            ['id', 'IN', $ruleIds],
            ['auth_type', '=', $authType]
        ], 'api_url,methods');
    }
}

七、添加新的鉴权规则

7.1 添加菜单权限

方式一:后台界面添加

  1. 登录平台后台
  2. 进入:设置 → 权限管理 → 菜单管理
  3. 点击”添加菜单”
  4. 填写信息:
    • 菜单名称
    • API路径(如:custom/list)
    • 请求方法(GET/POST/PUT/DELETE)
    • 权限类型(菜单/接口)

方式二:数据库直接添加

INSERT INTO `eb_system_menus` 
(`pid`, `menu_name`, `api_url`, `methods`, `auth_type`, `type`) 
VALUES 
(0, '自定义管理', '', 'GET', 1, 1),
(LAST_INSERT_ID(), '数据列表', 'custom/list', 'GET', 2, 1),
(LAST_INSERT_ID(), '添加数据', 'custom/save/:id', 'POST', 2, 1);

7.2 分配角色权限

// 创建新角色并分配权限
/** @var SystemRoleServices $roleService */
$roleService = app()->make(SystemRoleServices::class);

$roleData = [
    'role_name' => '自定义管理员',
    'rules' => '1,2,3,100,101,102',  // 权限菜单ID
    'level' => 1,
    'type' => 1,
    'status' => 1
];

$roleService->save($roleData);

7.3 自定义权限验证

<?php
// 在控制器中自定义权限验证

namespace app\controller\admin\v1\custom;

use app\controller\admin\AuthController;

class CustomController extends AuthController
{
    /**
     * 自定义权限验证
     */
    protected function checkCustomAuth($action)
    {
        $adminInfo = $this->request->adminInfo();

        // 超级管理员不验证
        if ($adminInfo['level'] == 0) {
            return true;
        }

        // 自定义权限逻辑
        $allowRoles = [1, 2, 3];  // 允许访问的角色ID
        $userRoles = explode(',', $adminInfo['roles']);

        if (empty(array_intersect($allowRoles, $userRoles))) {
            throw new AuthException('无访问权限');
        }

        return true;
    }

    public function sensitiveAction()
    {
        // 调用自定义权限验证
        $this->checkCustomAuth('sensitive');

        // 业务逻辑...
    }
}

八、前端Token处理

8.1 请求拦截器

// view/admin/src/plugins/request.js

import axios from 'axios';
import { getCookies, removeCookies } from '@/libs/util';
import Setting from '@/setting';

const service = axios.create({
    baseURL: Setting.apiBaseURL,
    timeout: 30000
});

// 请求拦截器
service.interceptors.request.use(
    config => {
        // 添加Token到请求头
        const token = getCookies('token');
        if (token) {
            config.headers['Authori-zation'] = 'Bearer ' + token;
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

// 响应拦截器
service.interceptors.response.use(
    response => {
        const res = response.data;

        // Token过期或无效
        if (res.status === 410000 || res.status === 410001) {
            removeCookies('token');
            // 跳转登录页
            window.location.href = '/admin/login';
            return Promise.reject(new Error(res.msg));
        }

        return res;
    },
    error => {
        return Promise.reject(error);
    }
);

export default service;

8.2 登录存储Token

// 登录方法
import { login } from '@/api/login';
import { setCookies } from '@/libs/util';

async handleLogin() {
    const { token, exp } = await login({
        account: this.form.account,
        password: this.form.password
    });

    // 存储Token
    setCookies('token', token, exp);

    // 跳转首页
    this.$router.push({ name: 'home' });
}

九、常见问题

Q1: Token无效或过期?

解决方案:

  1. 检查Token是否正确存储在Cookie或LocalStorage
  2. 检查请求头是否正确携带Token
  3. 检查Redis缓存服务是否正常
  4. 检查Token过期时间配置

Q2: 权限验证失败?

解决方案:

  1. 检查菜单权限是否已添加
  2. 检查角色是否已分配该权限
  3. 检查用户是否绑定了正确的角色
  4. 清除权限缓存后重试

Q3: 密码变更后Token仍有效?

解决方案:
Token中包含密码的MD5值,密码变更后Token验证会失败。确保:

  1. 生成Token时传入了正确的密码MD5
  2. 验证Token时比对了密码MD5

Q4: 如何实现单点登录?

// 登录时清除该用户的所有旧Token
public function login($account, $password)
{
    // ... 验证逻辑

    // 清除旧Token
    $this->clearUserAllTokens($adminInfo['id']);

    // 生成新Token
    return $this->createToken($adminInfo);
}

protected function clearUserAllTokens($userId)
{
    // 实现清除逻辑
    CacheService::delByPattern('token_bucket_*_user_' . $userId);
}

十、安全建议

  1. Token安全

    • 使用HTTPS传输
    • Token设置合理过期时间
    • 敏感操作需二次验证
  2. 密码安全

    • 使用password_hash加密存储
    • 限制登录错误次数
    • 支持密码复杂度验证
  3. 接口安全

    • 所有接口都需要权限验证
    • 敏感数据接口增加额外验证
    • 记录操作日志
{{cateWiki.like_num}}人点赞
0人点赞
评论({{cateWiki.comment_num}}) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} {{cateWiki.page_view_num}}人看过该文档
评论(0) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} 12人看过该文档
评论
{{item.user ? item.user.nickname : ''}} (自评)
{{item.content}}
{{item.create_time}} 删除
{{item.like ? item.like.like_num : 0}} {{replyIndex == index ? '取消回复' : '回复'}}
评论
{{items.user ? items.user.nickname : '暂无昵称'}} (自评)
{{items.content}}
{{items.create_time}} 删除
{{items.like ? items.like.like_num : 0}} {{replyIndexJ == (index+'|'+indexJ) ? '取消回复' : '回复'}}
评论
目录
  • {{item}}