公共登录 - 手机后、账号同时可作为登录条件

This commit is contained in:
柳清爽
2025-04-30 14:52:51 +08:00
parent f75c5fc310
commit df72ab8095
8 changed files with 275 additions and 186 deletions

View File

@@ -6,17 +6,17 @@ use think\facade\Route;
// 定义RESTful风格的API路由 - 认证相关
Route::group('v1/auth', function () {
// 无需认证的接口
Route::post('login', 'app\\common\\controller\\Auth@login'); // 账号密码登录
Route::post('mobile-login', 'app\\common\\controller\\Auth@mobileLogin'); // 手机号验证码登录
Route::post('code', 'app\\common\\controller\\Auth@sendCode'); // 发送验证码
Route::post('login', 'app\common\controller\PasswordLoginController@index'); // 账号密码登录
Route::post('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
// 需要JWT认证的接口
Route::get('info', 'app\\common\\controller\\Auth@info')->middleware(['jwt']); // 获取用户信息
Route::post('refresh', 'app\\common\\controller\\Auth@refresh')->middleware(['jwt']); // 刷新令牌
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
});
// 附件上传相关路由
Route::group('v1/', function () {
Route::post('attachment/upload', 'app\\common\\controller\\Attachment@upload'); // 上传附件
Route::get('attachment/:id', 'app\\common\\controller\\Attachment@info'); // 获取附件信息
Route::post('attachment/upload', 'app\common\controller\Attachment@upload'); // 上传附件
Route::get('attachment/:id', 'app\common\controller\Attachment@info'); // 获取附件信息
})->middleware(['jwt']);

View File

@@ -0,0 +1,12 @@
<?php
namespace app\common\controller;
use think\Controller;
/**
* 基础控制器
*/
class BaseController extends Controller
{
}

View File

@@ -0,0 +1,130 @@
<?php
namespace app\common\controller;
use app\common\model\User as UserModel;
use app\common\util\JwtUtil;
use Exception;
use library\ResponseHelper;
use think\response\Json;
use think\Validate;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class PasswordLoginController extends BaseController
{
/**
* 获取用户基本信息
*
* @param string $account
* @param int $typeId
* @return UserModel
*/
protected function getUserProfileWithAccountAndType(string $account, int $typeId): UserModel
{
$user = UserModel::where(function ($query) use ($account) {
$query->where('phone', $account)->whereOr('account', $account);
})
->where(function ($query) use ($typeId) {
$query->where('status', 1)->where('typeId', $typeId);
})->find();
return $user;
}
/**
* 获取用户信息
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param int $typeId 身份信息
* @return array|null
*/
protected function getUser(string $account, string $password, int $typeId): array
{
$user = $this->getUserProfileWithAccountAndType($account, $typeId);
if (!$user) {
throw new \Exception('用户不存在或已禁用', 403);
}
if ($user->passwordMd5 !== md5($password)) {
throw new \Exception('账号或密码错误', 403);
}
return $user->toArray();
}
/**
* 数据验证
*
* @param array $params
* @return $this
* @throws \Exception
*/
protected function dataValidate(array $params): self
{
$validate = Validate::make([
'account' => 'require',
'password' => 'require|length:6,64',
'typeId' => 'require|in:1,2',
], [
'account.require' => '账号不能为空',
'password.require' => '密码不能为空',
'password.length' => '密码长度必须在6-64个字符之间',
'typeId.require' => '用户类型不能为空',
'typeId.in' => '用户类型错误',
]);
if (!$validate->check($params)) {
throw new \Exception($validate->getError(), 400);
}
return $this;
}
/**
* 用户登录
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param string $typeId 登录IP
* @return array
* @throws \Exception
*/
protected function doLogin(string $account, string $password, int $typeId): array
{
// 获取用户信息
$member = $this->getUser($account, $password, $typeId);
// 生成JWT令牌
$token = JwtUtil::createToken($member, 7200);
$token_expired = time() + 7200;
return compact('member', 'token', 'token_expired');
}
/**
* 用户登录
*
* @return Json
*/
public function index()
{
$params = $this->request->only(['account', 'password', 'typeId']);
try {
$result = $this->dataValidate($params)->doLogin(
$params['account'],
$params['password'],
$params['typeId']
);
return ResponseHelper::success($result, '登录成功');
} catch (Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode());
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace app\common\controller;
use library\ResponseHelper;
use think\facade\Request;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class SendCodeController extends BaseController
{
/**
* 发送验证码
* @return \think\response\Json
*/
public function index()
{
$params = $this->request->only(['account', 'type']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('send_code')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 调用发送验证码服务
$result = $this->authService->sendLoginCode(
$params['account'],
$params['type']
);
return ResponseHelper::success($result, '验证码发送成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace app\common\model;
use think\Model;
@@ -7,84 +8,23 @@ use think\model\concern\SoftDelete;
class User extends Model
{
use SoftDelete;
/**
* 数据表名
* @var string
*/
protected $table = 'ck_users';
protected $name = 'users';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
protected $defaultSoftDelete = 0;
/**
* 主键
* @var string
*/
protected $pk = 'id';
/**
* 软删除字段
* @var string
*/
protected $deleteTime = 'deleteTime';
protected $defaultSoftDelete = 0;
/**
* 隐藏属性
* @var array
*/
protected $hidden = ['passwordMd5', 'passwordLocal', 'deleteTime'];
/**
* 字段类型
* @var array
*/
protected $type = [
'id' => 'integer',
'isAdmin' => 'integer',
'companyId' => 'integer',
'typeId' => 'integer',
'lastLoginTime' => 'integer',
'status' => 'integer',
'createTime' => 'integer',
'updateTime' => 'integer',
'deleteTime' => 'integer'
];
/**
* 通过手机号获取用户信息
* @param string $account 手机号
* @return array|null
*/
public static function getUserByMobile($account)
{
// 查询用户
$user = self::where('account', $account)
->where('status', 1)
->find();
if (!$user) {
return null;
}
// 用手机号当做默认用户名(如果没有设置用户名)
$username = $user->username ?: $user->account;
// 默认头像地址
$avatar = $user->avatar ?: '';
return [
'id' => $user->id,
'username' => $username,
'account' => $user->account,
'avatar' => $avatar,
'isAdmin' => $user->isAdmin,
'companyId' => $user->companyId,
'typeId' => $user->typeId,
'lastLoginIp' => $user->lastLoginIp,
'lastLoginTime' => $user->lastLoginTime
];
}
}

View File

@@ -1,85 +1,59 @@
<?php
namespace app\common\service;
use app\common\model\User;
use app\common\model\User as UserModel;
use app\common\util\JwtUtil;
use think\facade\Log;
use think\facade\Cache;
use think\facade\Config;
use think\facade\Env;
use think\facade\Log;
class AuthService
{
/**
* 令牌有效期(秒)
*/
const TOKEN_EXPIRE = 7200;
/**
* 短信服务实例
* @var SmsService
*/
protected $smsService;
/**
* 获取用户基本信息
*
* @param string $account
* @param int $typeId
* @return UserModel
*/
protected function getUserProfileWithAccountAndType(string $account, int $typeId): UserModel
{
$user = UserModel::where(function ($query) use ($account) {
$query->where('phone', $account)->whereOr('account', $account);
})
->where(function ($query) use ($typeId) {
$query->where('status', 1)->where('typeId', $typeId);
})->find();
return $user;
}
/**
* 获取用户信息
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param int $typeId 身份信息
* @return array|null
*/
protected function getUser($account, $password, $typeId)
protected function getUser(string $account, string $password, int $typeId): array
{
// 查询用户
$user = User::where('account', $account)
->where('typeId', $typeId)
->where('status', 1)
->find();
$user = $this->getUserProfileWithAccountAndType($account, $typeId);
if (!$user) {
// 记录日志
\think\facade\Log::info('用户不存在或已禁用', ['account' => $account]);
return null;
throw new \Exception('用户不存在或已禁用', 403);
}
// 记录密码验证信息
\think\facade\Log::info('密码验证', [
'account' => $account,
'input_password' => $password,
'stored_hash' => $user->passwordMd5,
]);
// 验证密码
$isValid = ($user->passwordMd5 == md5($password));
\think\facade\Log::info('密码验证结果', [
'account' => $account,
'is_valid' => $isValid,
]);
if (!$isValid) {
return null;
if ($user->passwordMd5 !== md5($password)) {
throw new \Exception('账号或密码错误', 403);
}
// 更新登录信息
$user->lastLoginIp = request()->ip();
$user->lastLoginTime = time();
$user->save();
// 用手机号当做默认用户名(如果没有设置用户名)
$username = $user->username ?: $user->account;
return [
'id' => $user->id,
'username' => $username,
'account' => $user->account,
'avatar' => $user->avatar,
'isAdmin' => $user->isAdmin,
'companyId' => $user->companyId,
'typeId' => $user->typeId,
'lastLoginIp' => $user->lastLoginIp,
'lastLoginTime' => $user->lastLoginTime
];
return $user->toArray();
}
/**
@@ -92,39 +66,28 @@ class AuthService
/**
* 用户登录
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param string $ip 登录IP
* @return array
* @throws \Exception
*/
public function login($account, $password, $typeId, $ip)
public function login(string $account, string $password, int $typeId, string $ip)
{
// 获取用户信息
$user = $this->getUser($account, $password, $typeId);
$member = $this->getUser($account, $password, $typeId);
if (empty($user)) {
// 记录登录失败
Log::info('登录失败', ['account' => $account, 'ip' => $ip, 'is_encrypted' => true]);
throw new \Exception('账号或密码错误');
}
// 生成JWT令牌
$token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
$expireTime = time() + self::TOKEN_EXPIRE;
// 记录登录成功
Log::info('登录成功', ['account' => $account, 'ip' => $ip]);
return [
'token' => $token,
'token_expired' => $expireTime,
'member' => $user
];
$token_expired = time() + self::TOKEN_EXPIRE;
return compact('member', 'token', 'token_expired');
}
/**
* 手机号验证码登录
*
* @param string $account 手机号
* @param string $code 验证码(可能是加密后的)
* @param string $ip 登录IP
@@ -137,14 +100,14 @@ class AuthService
// 验证验证码
if (!$this->smsService->verifyCode($account, $code, 'login', $isEncrypted)) {
Log::info('验证码验证失败', ['account' => $account, 'ip' => $ip, 'is_encrypted' => $isEncrypted]);
throw new \Exception('验证码错误或已过期');
throw new \Exception('验证码错误或已过期', 404);
}
// 获取用户信息
$user = User::getUserByMobile($account);
if (empty($user)) {
Log::info('用户不存在', ['account' => $account, 'ip' => $ip]);
throw new \Exception('用户不存在');
throw new \Exception('用户不存在', 404);
}
// 生成JWT令牌
@@ -163,6 +126,7 @@ class AuthService
/**
* 发送登录验证码
*
* @param string $account 手机号
* @param string $type 验证码类型
* @return array
@@ -175,6 +139,7 @@ class AuthService
/**
* 获取用户信息
*
* @param array $userInfo JWT中的用户信息
* @return array
* @throws \Exception
@@ -184,16 +149,17 @@ class AuthService
if (empty($userInfo)) {
throw new \Exception('获取用户信息失败');
}
// 移除不需要返回的字段
unset($userInfo['exp']);
unset($userInfo['iat']);
return $userInfo;
}
/**
* 刷新令牌
*
* @param array $userInfo JWT中的用户信息
* @return array
* @throws \Exception
@@ -203,15 +169,15 @@ class AuthService
if (empty($userInfo)) {
throw new \Exception('刷新令牌失败');
}
// 移除过期时间信息
unset($userInfo['exp']);
unset($userInfo['iat']);
// 生成新令牌
$token = JwtUtil::createToken($userInfo, self::TOKEN_EXPIRE);
$expireTime = time() + self::TOKEN_EXPIRE;
return [
'token' => $token,
'token_expired' => $expireTime
@@ -220,52 +186,53 @@ class AuthService
/**
* 获取系统授权信息使用缓存存储10分钟
*
* @return string
*/
public static function getSystemAuthorization()
{
// 定义缓存键名
$cacheKey = 'system_authorization_token';
// 尝试从缓存获取授权信息
$authorization = Cache::get($cacheKey);
//$authorization = 'mYpVVhPY7PxctvYw1pn1VCTS2ck0yZG8q11gAiJrRN_D3q7KXXBPAfXoAmqs7kKHeaAx-h4GB7DiqVIQJ09HiXVhaQT6PtgLX3w8YV16erThC-lG1fyJB4DJxu-QxA3Q8ogSs1WFOa8aAXD1QQUZ7Kbjkw_VMLL4lrfe0Yjaqy3DnO7aL1xGnNjjX8P5uqCAZgHKlN8NjuDEGyYvXygW1YyoK9pNpwvq-6DYKjLWdmbHvFaAybHf-hU1XyrFavZqcZYxIoVXjfJ5ASp4XxeCWqMCzwtSoz9RAvwLAlNxGweowtuyX9389ZaXI-zbqb2T0S8llg';
// 如果缓存中没有或已过期,则重新获取
// 如果缓存中没有或已过期,则重新获取
if (empty($authorization)) {
try {
// 从环境变量中获取API用户名和密码
$username = Env::get('api.username', '');
$password = Env::get('api.password', '');
if (empty($username) || empty($password)) {
Log::error('缺少API用户名或密码配置');
return '';
}
// 构建登录参数
$params = [
'grant_type' => 'password',
'username' => $username,
'password' => $password
];
// 获取API基础URL
$baseUrl = Env::get('api.wechat_url', '');
if (empty($baseUrl)) {
Log::error('缺少API基础URL配置');
return '';
}
// 调用登录接口获取token
// 设置请求头
$headerData = ['client:system'];
$header = setHeader($headerData, '', 'plain');
$result = requestCurl($baseUrl . 'token', $params, 'POST',$header);
$result = requestCurl($baseUrl . 'token', $params, 'POST', $header);
$result_array = handleApiResponse($result);
if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
$authorization = $result_array['access_token'];
// 存入缓存有效期10分钟600秒
Cache::set($cacheKey, $authorization, 600);
Cache::set('system_refresh_token', $result_array['refresh_token'], 600);
@@ -281,7 +248,7 @@ class AuthService
return '';
}
}
return $authorization;
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace app\common\service;
use think\facade\Cache;
@@ -13,12 +14,12 @@ class SmsService
* 验证码有效期(秒)
*/
const CODE_EXPIRE = 300;
/**
* 验证码长度
*/
const CODE_LENGTH = 4;
/**
* 发送验证码
* @param string $mobile 手机号
@@ -30,23 +31,23 @@ class SmsService
{
// 检查发送频率限制
$this->checkSendLimit($mobile, $type);
// 生成验证码
$code = $this->generateCode();
// 缓存验证码
$this->saveCode($mobile, $code, $type);
// 发送验证码(实际项目中对接短信平台)
$this->doSend($mobile, $code, $type);
// 记录日志
Log::info('发送验证码', [
'mobile' => $mobile,
'type' => $type,
'code' => $code
]);
return [
'mobile' => $mobile,
'expire' => self::CODE_EXPIRE,
@@ -54,7 +55,7 @@ class SmsService
'code' => $code
];
}
/**
* 验证验证码
* @param string $mobile 手机号
@@ -67,7 +68,7 @@ class SmsService
{
$cacheKey = $this->getCodeCacheKey($mobile, $type);
$cacheCode = Cache::get($cacheKey);
if (!$cacheCode) {
Log::info('验证码不存在或已过期', [
'mobile' => $mobile,
@@ -75,15 +76,15 @@ class SmsService
]);
return false;
}
// 验证码是否匹配
$isValid = false;
if ($isEncrypted) {
// 前端已加密,需要对缓存中的验证码进行相同的加密处理
$encryptedCacheCode = $this->encryptCode($cacheCode);
$isValid = hash_equals($encryptedCacheCode, $code);
// 记录日志
Log::info('加密验证码验证', [
'mobile' => $mobile,
@@ -95,7 +96,7 @@ class SmsService
} else {
// 未加密,直接比较
$isValid = ($cacheCode === $code);
// 记录日志
Log::info('明文验证码验证', [
'mobile' => $mobile,
@@ -104,15 +105,15 @@ class SmsService
'is_valid' => $isValid
]);
}
// 验证成功后删除缓存
if ($isValid) {
Cache::rm($cacheKey);
}
return $isValid;
}
/**
* 检查发送频率限制
* @param string $mobile 手机号
@@ -122,24 +123,24 @@ class SmsService
protected function checkSendLimit($mobile, $type)
{
$cacheKey = $this->getCodeCacheKey($mobile, $type);
// 检查是否存在未过期的验证码
if (Cache::has($cacheKey)) {
throw new \Exception('验证码已发送,请稍后再试');
}
// 检查当日发送次数限制
$limitKey = "sms_limit:{$mobile}:" . date('Ymd');
$sendCount = Cache::get($limitKey, 0);
if ($sendCount >= 10) {
throw new \Exception('今日发送次数已达上限');
}
// 更新发送次数
Cache::set($limitKey, $sendCount + 1, 86400);
}
/**
* 生成随机验证码
* @return string
@@ -149,7 +150,7 @@ class SmsService
// 生成4位数字验证码
return sprintf("%0" . self::CODE_LENGTH . "d", mt_rand(0, pow(10, self::CODE_LENGTH) - 1));
}
/**
* 保存验证码到缓存
* @param string $mobile 手机号
@@ -161,7 +162,7 @@ class SmsService
$cacheKey = $this->getCodeCacheKey($mobile, $type);
Cache::set($cacheKey, $code, self::CODE_EXPIRE);
}
/**
* 执行发送验证码
* @param string $mobile 手机号
@@ -175,7 +176,7 @@ class SmsService
// 这里仅做模拟,返回成功
return true;
}
/**
* 获取验证码缓存键名
* @param string $mobile 手机号
@@ -186,7 +187,7 @@ class SmsService
{
return "sms_code:{$mobile}:{$type}";
}
/**
* 加密验证码
* 使用与前端相同的加密算法

View File

@@ -32,7 +32,7 @@ class BaseController extends Controller
* @return mixed
* @throws \Exception
*/
protected function getUserInfo(string $column = '')
protected function getUserInfo(?string $column = null)
{
$user = $this->request->userInfo;