{{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 添加菜单权限
方式一:后台界面添加
- 登录平台后台
- 进入:设置 → 权限管理 → 菜单管理
- 点击”添加菜单”
- 填写信息:
- 菜单名称
- 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无效或过期?
解决方案:
- 检查Token是否正确存储在Cookie或LocalStorage
- 检查请求头是否正确携带Token
- 检查Redis缓存服务是否正常
- 检查Token过期时间配置
Q2: 权限验证失败?
解决方案:
- 检查菜单权限是否已添加
- 检查角色是否已分配该权限
- 检查用户是否绑定了正确的角色
- 清除权限缓存后重试
Q3: 密码变更后Token仍有效?
解决方案:
Token中包含密码的MD5值,密码变更后Token验证会失败。确保:
- 生成Token时传入了正确的密码MD5
- 验证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);
}
十、安全建议
Token安全
- 使用HTTPS传输
- Token设置合理过期时间
- 敏感操作需二次验证
密码安全
- 使用password_hash加密存储
- 限制登录错误次数
- 支持密码复杂度验证
接口安全
- 所有接口都需要权限验证
- 敏感数据接口增加额外验证
- 记录操作日志
评论({{cateWiki.comment_num}})
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
{{cateWiki.page_view_num}}人看过该文档
评论(0)
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
12人看过该文档
{{item.user ? item.user.nickname : ''}}
(自评)
{{item.content}}
{{item.create_time}}
删除
搜索结果
为您找到{{wikiCount}}条结果
{{item.page_view_num}}
{{item.like ? item.like.like_num : 0}}
{{item.comment ? item.comment.comment_num : 0}}
位置:
{{path.name}}
{{(i+1) == item.catalogue.path_data.length ? '':'/'}}