【私域操盘手】账号密码登录

This commit is contained in:
eison
2025-03-16 17:43:30 +08:00
parent f4e36f1921
commit 1d7a87f29f
30 changed files with 1474 additions and 97 deletions

View File

@@ -0,0 +1,25 @@
<?php
// common模块路由配置
use think\facade\Route;
// 添加测试路由
Route::get('api/test', function() {
return json([
'code' => 200,
'msg' => '路由测试成功',
'data' => [
'time' => date('Y-m-d H:i:s'),
'module' => 'common'
]
]);
});
// 定义RESTful风格的API路由
Route::post('api/auth/login', 'app\\common\\controller\\Auth@login'); // 登录接口
// 需要JWT认证的接口
Route::get('api/auth/info', 'app\\common\\controller\\Auth@info')->middleware(['jwt']); // 获取用户信息
Route::post('api/auth/refresh', 'app\\common\\controller\\Auth@refresh')->middleware(['jwt']); // 刷新令牌
return [];

View File

@@ -0,0 +1,104 @@
<?php
namespace app\common\controller;
use app\common\helper\ResponseHelper;
use app\common\service\AuthService;
use think\Controller;
use think\facade\Request;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class Auth extends Controller
{
/**
* 允许跨域请求的域名
* @var string
*/
protected $allowOrigin = '*';
/**
* 认证服务实例
* @var AuthService
*/
protected $authService;
/**
* 初始化
* 设置跨域相关响应头
*/
public function initialize()
{
parent::initialize();
// 允许跨域访问
header('Access-Control-Allow-Origin: ' . $this->allowOrigin);
header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
// 预检请求直接返回200
if (Request::method(true) == 'OPTIONS') {
exit();
}
// 初始化认证服务
$this->authService = new AuthService();
}
/**
* 用户登录
* @return \think\response\Json
*/
public function login()
{
// 获取登录参数
$params = Request::only(['username', 'password']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('login')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 调用登录服务
$result = $this->authService->login(
$params['username'],
$params['password'],
Request::ip()
);
return ResponseHelper::success($result, '登录成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
/**
* 获取用户信息
* @return \think\response\Json
*/
public function info()
{
try {
$result = $this->authService->getUserInfo(request()->userInfo);
return ResponseHelper::success($result);
} catch (\Exception $e) {
return ResponseHelper::unauthorized($e->getMessage());
}
}
/**
* 刷新令牌
* @return \think\response\Json
*/
public function refresh()
{
try {
$result = $this->authService->refreshToken(request()->userInfo);
return ResponseHelper::success($result, '刷新成功');
} catch (\Exception $e) {
return ResponseHelper::unauthorized($e->getMessage());
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace app\common\helper;
class ResponseHelper
{
/**
* 成功响应
* @param mixed $data 响应数据
* @param string $msg 响应消息
* @param int $code 响应代码
* @return \think\response\Json
*/
public static function success($data = null, $msg = '操作成功', $code = 200)
{
return json([
'code' => $code,
'msg' => $msg,
'data' => $data
]);
}
/**
* 错误响应
* @param string $msg 错误消息
* @param int $code 错误代码
* @param mixed $data 错误数据
* @return \think\response\Json
*/
public static function error($msg = '操作失败', $code = 400, $data = null)
{
return json([
'code' => $code,
'msg' => $msg,
'data' => $data
]);
}
/**
* 未授权响应
* @param string $msg 错误消息
* @return \think\response\Json
*/
public static function unauthorized($msg = '未授权访问')
{
return self::error($msg, 401);
}
/**
* 禁止访问响应
* @param string $msg 错误消息
* @return \think\response\Json
*/
public static function forbidden($msg = '禁止访问')
{
return self::error($msg, 403);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace app\common\model;
use think\Model;
class User extends Model
{
/**
* 数据表名
* @var string
*/
protected $table = 'user';
/**
* 自动写入时间戳
* @var bool
*/
protected $autoWriteTimestamp = true;
/**
* 创建时间字段
* @var string
*/
protected $createTime = 'create_time';
/**
* 更新时间字段
* @var string
*/
protected $updateTime = 'update_time';
/**
* 隐藏属性
* @var array
*/
protected $hidden = ['password', 'delete_time'];
/**
* 获取管理员用户信息
* @param string $username 用户名
* @param string $password 密码
* @return array|null
*/
public static function getAdminUser($username, $password)
{
// 目前使用固定账号,后续可改为数据库查询
if ($username === 'admin' && $password === '123456') {
return [
'id' => 1,
'username' => 'admin',
'name' => '超级管理员',
'role' => 'admin',
'permissions' => ['*'], // 拥有所有权限
];
}
return null;
}
/**
* 通过手机号获取用户信息
* @param string $mobile 手机号
* @return array|null
*/
public static function getUserByMobile($mobile)
{
// 目前使用固定账号,后续可改为数据库查询
if ($mobile === '13800138000') {
return [
'id' => 2,
'username' => 'mobile_user',
'name' => '手机用户',
'mobile' => '13800138000',
'role' => 'user',
'permissions' => ['user'], // 普通用户权限
];
}
return null;
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace app\common\service;
use app\common\model\User;
use app\common\util\JwtUtil;
use think\facade\Log;
class AuthService
{
/**
* 令牌有效期(秒)
*/
const TOKEN_EXPIRE = 7200;
/**
* 短信服务实例
* @var SmsService
*/
protected $smsService;
/**
* 构造函数
*/
public function __construct()
{
$this->smsService = new SmsService();
}
/**
* 用户登录
* @param string $username 用户名
* @param string $password 密码
* @param string $ip 登录IP
* @return array
* @throws \Exception
*/
public function login($username, $password, $ip)
{
// 获取用户信息
$user = User::getAdminUser($username, $password);
if (empty($user)) {
// 记录登录失败
Log::info('登录失败', ['username' => $username, 'ip' => $ip]);
throw new \Exception('用户名或密码错误');
}
// 生成JWT令牌
$token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
$expireTime = time() + self::TOKEN_EXPIRE;
// 记录登录成功
Log::info('登录成功', ['username' => $username, 'ip' => $ip]);
return [
'token' => $token,
'token_expired' => $expireTime,
'member' => $user
];
}
/**
* 手机号验证码登录
* @param string $mobile 手机号
* @param string $code 验证码
* @param string $ip 登录IP
* @return array
* @throws \Exception
*/
public function mobileLogin($mobile, $code, $ip)
{
// 验证验证码
if (!$this->smsService->verifyCode($mobile, $code, 'login')) {
Log::info('验证码验证失败', ['mobile' => $mobile, 'ip' => $ip]);
throw new \Exception('验证码错误或已过期');
}
// 获取用户信息
$user = User::getUserByMobile($mobile);
if (empty($user)) {
Log::info('用户不存在', ['mobile' => $mobile, 'ip' => $ip]);
throw new \Exception('用户不存在');
}
// 生成JWT令牌
$token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
$expireTime = time() + self::TOKEN_EXPIRE;
// 记录登录成功
Log::info('手机号登录成功', ['mobile' => $mobile, 'ip' => $ip]);
return [
'token' => $token,
'token_expired' => $expireTime,
'member' => $user
];
}
/**
* 发送登录验证码
* @param string $mobile 手机号
* @param string $type 验证码类型
* @return array
* @throws \Exception
*/
public function sendLoginCode($mobile, $type)
{
return $this->smsService->sendCode($mobile, $type);
}
/**
* 获取用户信息
* @param array $userInfo JWT中的用户信息
* @return array
* @throws \Exception
*/
public function getUserInfo($userInfo)
{
if (empty($userInfo)) {
throw new \Exception('获取用户信息失败');
}
// 移除不需要返回的字段
unset($userInfo['exp']);
unset($userInfo['iat']);
return $userInfo;
}
/**
* 刷新令牌
* @param array $userInfo JWT中的用户信息
* @return array
* @throws \Exception
*/
public function refreshToken($userInfo)
{
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
];
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace app\common\service;
use think\facade\Cache;
use think\facade\Log;
class SmsService
{
/**
* 验证码有效期(秒)
*/
const CODE_EXPIRE = 300;
/**
* 发送验证码
* @param string $mobile 手机号
* @param string $type 验证码类型
* @return array
* @throws \Exception
*/
public function sendCode($mobile, $type)
{
// 检查发送频率
$this->checkSendFrequency($mobile);
// 生成验证码
$code = $this->generateCode();
try {
// TODO: 对接实际的短信发送服务
// 这里模拟发送成功
$this->saveCode($mobile, $code, $type);
// 记录发送日志
Log::info('验证码发送成功', [
'mobile' => $mobile,
'type' => $type,
'code' => $code
]);
return [
'mobile' => $mobile,
'expire' => self::CODE_EXPIRE
];
} catch (\Exception $e) {
Log::error('验证码发送失败', [
'mobile' => $mobile,
'type' => $type,
'error' => $e->getMessage()
]);
throw new \Exception('验证码发送失败,请稍后重试');
}
}
/**
* 验证验证码
* @param string $mobile 手机号
* @param string $code 验证码
* @param string $type 验证码类型
* @return bool
*/
public function verifyCode($mobile, $code, $type)
{
$key = $this->getCodeKey($mobile, $type);
$savedCode = Cache::get($key);
if (!$savedCode || $savedCode !== $code) {
return false;
}
// 验证成功后删除验证码
Cache::rm($key);
return true;
}
/**
* 检查发送频率
* @param string $mobile 手机号
* @throws \Exception
*/
protected function checkSendFrequency($mobile)
{
$key = 'sms_frequency_' . $mobile;
$lastSendTime = Cache::get($key);
if ($lastSendTime && time() - $lastSendTime < 60) {
throw new \Exception('发送太频繁,请稍后再试');
}
Cache::set($key, time(), 60);
}
/**
* 生成验证码
* @return string
*/
protected function generateCode()
{
return sprintf('%06d', random_int(0, 999999));
}
/**
* 保存验证码
* @param string $mobile 手机号
* @param string $code 验证码
* @param string $type 验证码类型
*/
protected function saveCode($mobile, $code, $type)
{
$key = $this->getCodeKey($mobile, $type);
Cache::set($key, $code, self::CODE_EXPIRE);
}
/**
* 获取缓存key
* @param string $mobile 手机号
* @param string $type 验证码类型
* @return string
*/
protected function getCodeKey($mobile, $type)
{
return 'sms_code_' . $type . '_' . $mobile;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace app\common\util;
use think\facade\Config;
use think\facade\Request;
/**
* JWT工具类
* 用于生成和验证JWT令牌
*/
class JwtUtil
{
/**
* 密钥
* @var string
*/
protected static $secret = 'YiShi@2023#JWT';
/**
* 头部
* @var array
*/
protected static $header = [
'alg' => 'HS256', // 加密算法
'typ' => 'JWT' // 类型
];
/**
* 创建JWT令牌
* @param array $payload 载荷信息
* @param int $expire 过期时间(秒)默认2小时
* @return string
*/
public static function createToken($payload, $expire = 7200)
{
$header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
// 附加过期时间
$payload['exp'] = time() + $expire;
$payload['iat'] = time(); // 签发时间
$payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
$signature = self::signature($header . '.' . $payload, self::$secret);
return $header . '.' . $payload . '.' . $signature;
}
/**
* 验证令牌
* @param string $token 令牌
* @return array|bool 验证通过返回载荷信息失败返回false
*/
public static function verifyToken($token)
{
if (empty($token)) {
return false;
}
$tokenArray = explode('.', $token);
if (count($tokenArray) != 3) {
return false;
}
list($header, $payload, $signature) = $tokenArray;
// 验证签名
if (self::signature($header . '.' . $payload, self::$secret) !== $signature) {
return false;
}
// 解码载荷
$payload = json_decode(self::base64UrlDecode($payload), true);
// 验证是否过期
if (isset($payload['exp']) && $payload['exp'] < time()) {
return false;
}
return $payload;
}
/**
* 生成签名
* @param string $input 输入
* @param string $key 密钥
* @return string
*/
private static function signature($input, $key)
{
return self::base64UrlEncode(hash_hmac('sha256', $input, $key, true));
}
/**
* URL安全的Base64编码
* @param string $input
* @return string
*/
private static function base64UrlEncode($input)
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($input));
}
/**
* URL安全的Base64解码
* @param string $input
* @return string
*/
private static function base64UrlDecode($input)
{
$remainder = strlen($input) % 4;
if ($remainder) {
$input .= str_repeat('=', 4 - $remainder);
}
return base64_decode(str_replace(['-', '_'], ['+', '/'], $input));
}
/**
* 从请求头中获取Token
* @return string|null
*/
public static function getRequestToken()
{
$authorization = Request::header('Authorization');
if (!$authorization) {
return null;
}
// 检查Bearer前缀
if (strpos($authorization, 'Bearer ') !== 0) {
return null;
}
return substr($authorization, 7);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace app\common\validate;
use think\Validate;
class Auth extends Validate
{
/**
* 验证规则
* @var array
*/
protected $rule = [
'username' => 'require|length:3,32',
'password' => 'require|length:6,32',
'mobile' => 'require|mobile',
'code' => 'require|length:6',
'type' => 'require|in:login,register',
];
/**
* 错误信息
* @var array
*/
protected $message = [
'username.require' => '用户名不能为空',
'username.length' => '用户名长度必须在3-32个字符之间',
'password.require' => '密码不能为空',
'password.length' => '密码长度必须在6-32个字符之间',
'mobile.require' => '手机号不能为空',
'mobile.mobile' => '手机号格式不正确',
'code.require' => '验证码不能为空',
'code.length' => '验证码必须是6位数字',
'type.require' => '验证码类型不能为空',
'type.in' => '验证码类型不正确',
];
/**
* 验证场景
* @var array
*/
protected $scene = [
'login' => ['username', 'password'],
'mobile_login' => ['mobile', 'code'],
'send_code' => ['mobile', 'type'],
];
}