diff --git a/.Cursorignore b/.Cursorignore new file mode 100644 index 00000000..a78c99e4 --- /dev/null +++ b/.Cursorignore @@ -0,0 +1,9 @@ +Server/runtime/ +*.png +*.jpg +*.jpeg +*.gif +*.bmp +*.webp +*.ico +*.svg \ No newline at end of file diff --git a/Backend/.env b/Backend/.env index 41a988e0..75531f85 100755 --- a/Backend/.env +++ b/Backend/.env @@ -3,4 +3,6 @@ VUE_APP_PREVIEW=false VUE_APP_API_BASE_URL=http://yishi.com VUE_APP_WWW_BASE_URL=http://yishi.com VUE_APP_WEB_SOCKET_URL=ws://yishi.com:2348 -VUE_APP_WEBSITE_NAME="管理后台" \ No newline at end of file +VUE_APP_WEBSITE_NAME="管理后台" +VUE_APP_TITLE=医师管理系统 +VUE_APP_API_URL=http://yishi.com \ No newline at end of file diff --git a/Backend/.env.development b/Backend/.env.development index a7a8df28..f04e7d78 100755 --- a/Backend/.env.development +++ b/Backend/.env.development @@ -1,6 +1,7 @@ NODE_ENV=development -VUE_APP_PREVIEW=true -VUE_APP_API_BASE_URL=http://yishi.com -VUE_APP_WWW_BASE_URL=http://yishi.com -VUE_APP_WEB_SOCKET_URL=ws://yishi.com:2348 -VUE_APP_WEBSITE_NAME="管理后台" \ No newline at end of file +VUE_APP_PREVIEW=false +VUE_APP_API_BASE_URL=http://localhost:8000 +VUE_APP_WWW_BASE_URL=http://localhost:8000 +VUE_APP_WEB_SOCKET_URL=ws://localhost:2348 +VUE_APP_WEBSITE_NAME="医师管理系统开发环境" +VUE_APP_TITLE=医师管理系统 \ No newline at end of file diff --git a/Backend/src/api/user.js b/Backend/src/api/user.js index 2f4a44af..48582fea 100755 --- a/Backend/src/api/user.js +++ b/Backend/src/api/user.js @@ -1,12 +1,18 @@ -import { post } from '@/utils/request' +import { post, get } from '@/utils/request' // 登录服务接口 export const ServeLogin = data => { - return post('/backend/user/login', data) + return post('/api/auth/login', data) } +// 获取用户信息 export const ServeGetUser = () => { - return post('/backend/user/get') + return get('/api/auth/info') +} + +// 刷新token +export const ServeRefreshToken = () => { + return post('/api/auth/refresh') } export const ServeSetUserPassword = (data) => { @@ -14,8 +20,9 @@ export const ServeSetUserPassword = (data) => { } // 退出登录服务接口 -export const ServeLogout = data => { - return post('/backend/user/logout', data) +export const ServeLogout = () => { + // JWT不需要服务端登出,直接清除本地token即可 + return Promise.resolve({ code: 200, msg: '退出成功' }) } export const UserIndex = data => { diff --git a/Backend/src/router/auth.js b/Backend/src/router/auth.js index 729c249f..5d1b4e04 100755 --- a/Backend/src/router/auth.js +++ b/Backend/src/router/auth.js @@ -7,7 +7,7 @@ export default { { path: '/auth/login', meta: { - title: '账号登录?', + title: '账号登录', needLogin: false, }, component: () => import('@/views/auth/login'), diff --git a/Backend/src/router/index.js b/Backend/src/router/index.js index a37431d5..c73087f3 100755 --- a/Backend/src/router/index.js +++ b/Backend/src/router/index.js @@ -6,6 +6,8 @@ import SystemRouter from './system' import ProductRouter from '@/router/product'; import TaskRouter from '@/router/task'; import DeviceRouter from '@/router/device'; +import { isLogin } from '@/utils/auth' +import store from '@/store' const originalPush = Router.prototype.push Router.prototype.push = function push(location) { @@ -46,7 +48,33 @@ const routes = [ }, ] -export default new Router({ +const router = new Router({ routes, mode: 'hash', }) + +// 全局前置守卫 +router.beforeEach((to, from, next) => { + // 设置页面标题 + document.title = to.meta.title ? to.meta.title : '医视管理系统' + + // 检查路由是否需要登录 + if (to.meta.needLogin) { + // 检查登录状态 + if (isLogin()) { + next() + } else { + // 未登录,重定向到登录页 + next({ path: '/auth/login' }) + } + } else { + // 如果是访问登录页且已登录,则跳转到首页 + if (to.path === '/auth/login' && isLogin()) { + next({ path: '/' }) + } else { + next() + } + } +}) + +export default router diff --git a/Backend/src/store/modules/user.js b/Backend/src/store/modules/user.js index 280df559..e5c50a58 100755 --- a/Backend/src/store/modules/user.js +++ b/Backend/src/store/modules/user.js @@ -1,56 +1,55 @@ -import { setUserInfo, getUserInfo, removeAll, getToken } from '@/utils/auth' +import { setUserInfo, getUserInfo, removeAll, getToken, isLogin } from '@/utils/auth' -import { ServeLogout } from '@/api/user' +import { ServeLogout, ServeRefreshToken } from '@/api/user' let state = { // 用户ID id: 0, - // 渠道ID - channel_id: 0, - // 渠道名称 - channel_name: '', + // 角色 + role: '', + // 权限 + permissions: [], // 账号 username: '', // 姓名 name: '', - // 手机号 - mobile: '', - // 登录时间 - login_time: '', - // 登录次数 - login_count: 0, - // 登录IP - login_ip: '', - // 创建时间 - create_time: '', // 个性头像 avatar: require('@/assets/image/detault-avatar.jpg'), - // 角色 - roles: [], // 当前登录状态 loginStatus: false, - // 是否启动游戏模块 + // 原有字段保持不变 + channel_id: 0, + channel_name: '', + mobile: '', + login_time: '', + login_count: 0, + login_ip: '', + create_time: '', mod_game: false, } // 判断用户是否登录 -if (getToken()) { +if (isLogin()) { let userInfo = getUserInfo() - state.id = userInfo.id - state.channel_id = userInfo.channel_id - state.channel_name = userInfo.channel_name - state.username = userInfo.username - state.name = userInfo.name - state.mobile = userInfo.mobile - state.login_time = userInfo.login_time - state.login_count = userInfo.login_count - state.login_ip = userInfo.login_ip - state.create_time = userInfo.create_time - state.roles = userInfo.roles ? userInfo.roles: [] - //state.avatar = userInfo.avatar ? userInfo.avatar : state.avatar + // 更新状态 + state.id = userInfo.id + state.username = userInfo.username + state.name = userInfo.name + state.role = userInfo.role + state.permissions = userInfo.permissions || [] state.loginStatus = true - state.mod_game = userInfo.mod_game + + // 兼容原有字段 + state.channel_id = userInfo.channel_id || 0 + state.channel_name = userInfo.channel_name || '' + state.mobile = userInfo.mobile || '' + state.login_time = userInfo.login_time || '' + state.login_count = userInfo.login_count || 0 + state.login_ip = userInfo.login_ip || '' + state.create_time = userInfo.create_time || '' + state.mod_game = userInfo.mod_game || false + state.roles = userInfo.roles || [] console.log('userInfo: ', userInfo) } @@ -60,24 +59,28 @@ const User = { mutations: { // 用户退出登录 USER_LOGOUT(state) { - state.id = 0 - state.channel_id = 0 - state.channel_name = '' - state.username = '' - state.name = '' - state.mobile = '' - state.login_time = '' - state.login_count = 0 - state.login_ip = '' - state.create_time = '' - state.roles = [] + state.id = 0 + state.username = '' + state.name = '' + state.role = '' + state.permissions = [] state.loginStatus = false - state.mod_game = false + + // 原有字段重置 + state.channel_id = 0 + state.channel_name = '' + state.mobile = '' + state.login_time = '' + state.login_count = 0 + state.login_ip = '' + state.create_time = '' + state.roles = [] + state.mod_game = false }, // 设置用户登录状态 - UPDATE_LOGIN_STATUS(state) { - state.loginStatus = true + UPDATE_LOGIN_STATUS(state, status) { + state.loginStatus = status }, // 更新用户信息 @@ -91,18 +94,20 @@ const User = { // 保存用户信息到缓存 setUserInfo({ id: state.id, - channel_id: state.channel_id, - channel_name: state.channel_name, username: state.username, name: state.name, + role: state.role, + permissions: state.permissions || [], + // 兼容原有字段 + channel_id: state.channel_id, + channel_name: state.channel_name, mobile: state.mobile, login_time: state.login_time, login_count: state.login_count, login_ip: state.login_ip, create_time: state.create_time, - roles: state.roles ? state.roles: [], + roles: state.roles || [], mod_game: state.mod_game, - //avatar: state.avatar, }) }, }, @@ -115,6 +120,25 @@ const User = { location.reload() }) }, + + // 刷新令牌 + ACT_REFRESH_TOKEN({ commit }) { + return new Promise((resolve, reject) => { + ServeRefreshToken() + .then(res => { + if (res.code === 200) { + // 更新token + setToken(res.data.token, res.data.token_expired - Math.floor(Date.now() / 1000)) + resolve(res) + } else { + reject(res) + } + }) + .catch(error => { + reject(error) + }) + }) + }, }, } diff --git a/Backend/src/utils/auth.js b/Backend/src/utils/auth.js index 750065e6..ea517fd6 100755 --- a/Backend/src/utils/auth.js +++ b/Backend/src/utils/auth.js @@ -7,8 +7,8 @@ const USER_SETTING = 'MANAGE_SETTING' /** * 设置用户授权token * - * @param {String} token - * @param {Number} expires + * @param {String} token - JWT令牌 + * @param {Number} expires - 过期时间戳(秒) */ export function setToken(token, expires) { expires = new Date().getTime() + expires * 1000 @@ -23,6 +23,7 @@ export function setToken(token, expires) { /** * 获取授权token + * @returns {String} token */ export function getToken() { const result = JSON.parse( @@ -33,9 +34,24 @@ export function getToken() { }) ) + // 检查token是否过期 + if (result.expires > 0 && result.expires < new Date().getTime()) { + // token已过期,清除token + removeAll() + return '' + } + return result.token } +/** + * 检查用户是否登录 + * @returns {Boolean} + */ +export function isLogin() { + return !!getToken() +} + /** * 设置用户信息 * diff --git a/Backend/src/utils/request.js b/Backend/src/utils/request.js index d808a123..daa2d112 100755 --- a/Backend/src/utils/request.js +++ b/Backend/src/utils/request.js @@ -39,7 +39,8 @@ const errorHandler = error => { request.interceptors.request.use(config => { const token = getToken() if (token) { - config.headers['token'] = token + // 设置JWT认证头 + config.headers['Authorization'] = 'Bearer ' + token } return config diff --git a/Backend/src/views/auth/login.vue b/Backend/src/views/auth/login.vue index f190bab4..532b63d2 100755 --- a/Backend/src/views/auth/login.vue +++ b/Backend/src/views/auth/login.vue @@ -35,7 +35,7 @@ + + \ No newline at end of file diff --git a/Server/.env b/Server/.env new file mode 100644 index 00000000..c55ec3e5 --- /dev/null +++ b/Server/.env @@ -0,0 +1,14 @@ +[APP] +APP_DEBUG = true +APP_TRACE = false + +[DATABASE] +TYPE = mysql +HOSTNAME = localhost +DATABASE = yishi +USERNAME = root +PASSWORD = 123456 +HOSTPORT = 3306 +CHARSET = utf8mb4 +PREFIX = tk_ +DEBUG = true \ No newline at end of file diff --git a/Server/application/common/config/route.php b/Server/application/common/config/route.php new file mode 100644 index 00000000..c2b319f9 --- /dev/null +++ b/Server/application/common/config/route.php @@ -0,0 +1,25 @@ + 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 []; \ No newline at end of file diff --git a/Server/application/common/controller/Auth.php b/Server/application/common/controller/Auth.php new file mode 100644 index 00000000..787f93ec --- /dev/null +++ b/Server/application/common/controller/Auth.php @@ -0,0 +1,104 @@ +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()); + } + } +} \ No newline at end of file diff --git a/Server/application/common/helper/ResponseHelper.php b/Server/application/common/helper/ResponseHelper.php new file mode 100644 index 00000000..2b0966e2 --- /dev/null +++ b/Server/application/common/helper/ResponseHelper.php @@ -0,0 +1,57 @@ + $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); + } +} \ No newline at end of file diff --git a/Server/application/common/model/User.php b/Server/application/common/model/User.php new file mode 100644 index 00000000..5c995ac0 --- /dev/null +++ b/Server/application/common/model/User.php @@ -0,0 +1,79 @@ + 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; + } +} \ No newline at end of file diff --git a/Server/application/common/service/AuthService.php b/Server/application/common/service/AuthService.php new file mode 100644 index 00000000..e86d216a --- /dev/null +++ b/Server/application/common/service/AuthService.php @@ -0,0 +1,155 @@ +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 + ]; + } +} \ No newline at end of file diff --git a/Server/application/common/service/SmsService.php b/Server/application/common/service/SmsService.php new file mode 100644 index 00000000..35cd3f22 --- /dev/null +++ b/Server/application/common/service/SmsService.php @@ -0,0 +1,124 @@ +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; + } +} \ No newline at end of file diff --git a/Server/application/common/util/JwtUtil.php b/Server/application/common/util/JwtUtil.php new file mode 100644 index 00000000..2ba8fb5f --- /dev/null +++ b/Server/application/common/util/JwtUtil.php @@ -0,0 +1,135 @@ + '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); + } +} \ No newline at end of file diff --git a/Server/application/common/validate/Auth.php b/Server/application/common/validate/Auth.php new file mode 100644 index 00000000..ff381ad4 --- /dev/null +++ b/Server/application/common/validate/Auth.php @@ -0,0 +1,46 @@ + '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'], + ]; +} \ No newline at end of file diff --git a/Server/application/http/middleware/JwtAuth.php b/Server/application/http/middleware/JwtAuth.php new file mode 100644 index 00000000..7410302a --- /dev/null +++ b/Server/application/http/middleware/JwtAuth.php @@ -0,0 +1,49 @@ + 401, + 'msg' => '未授权访问,缺少有效的身份凭证', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + $payload = JwtUtil::verifyToken($token); + if (!$payload) { + return json([ + 'code' => 401, + 'msg' => '授权已过期或无效', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + // 将用户信息附加到请求中 + $request->userInfo = $payload; + + // 写入日志 + Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']); + + return $next($request); + } +} \ No newline at end of file diff --git a/Server/config/app.php b/Server/config/app.php index d8009b70..f0c83f5c 100755 --- a/Server/config/app.php +++ b/Server/config/app.php @@ -45,7 +45,7 @@ return [ // 默认语言 'default_lang' => 'zh-cn', // 应用类库后缀 - 'class_suffix' => true, + 'class_suffix' => false, // 控制器类后缀 'controller_suffix' => false, @@ -54,9 +54,9 @@ return [ // +---------------------------------------------------------------------- // 默认模块名 - 'default_module' => 'frontend', + 'default_module' => 'index', // 禁止访问模块 - 'deny_module_list' => ['common'], + 'deny_module_list' => [], // 默认控制器名 'default_controller' => 'Index', // 默认操作名 @@ -89,9 +89,9 @@ return [ // IP代理获取标识 'http_agent_ip' => 'X-REAL-IP', // URL伪静态后缀 - 'url_html_suffix' => '', + 'url_html_suffix' => 'html', // URL普通方式参数 用于自动生成 - 'url_common_param' => true, + 'url_common_param' => false, // URL参数方式 0 按名称成对解析 1 按顺序解析 'url_param_type' => 0, // 是否开启路由延迟解析 diff --git a/Server/config/database.php b/Server/config/database.php index 971ddc3c..8eb199f7 100755 --- a/Server/config/database.php +++ b/Server/config/database.php @@ -11,27 +11,27 @@ return [ // 数据库类型 - 'type' => 'mysql', + 'type' => env('database.type', 'mysql'), // 服务器地址 - 'hostname' => '127.0.0.1', + 'hostname' => env('database.hostname', '127.0.0.1'), // 数据库名 - 'database' => 'yishi', + 'database' => env('database.database', 'yishi'), // 用户名 - 'username' => 'yishi', + 'username' => env('database.username', 'root'), // 密码 - 'password' => 'KcankSjjdZ5CsTC7', + 'password' => env('database.password', '123456'), // 端口 - 'hostport' => '', + 'hostport' => env('database.hostport', '3306'), // 连接dsn 'dsn' => '', // 数据库连接参数 'params' => [], // 数据库编码默认采用utf8 - 'charset' => 'utf8mb4', + 'charset' => env('database.charset', 'utf8mb4'), // 数据库表前缀 - 'prefix' => 'tk_', + 'prefix' => env('database.prefix', 'tk_'), // 数据库调试模式 - 'debug' => true, + 'debug' => env('database.debug', true), // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) 'deploy' => 0, // 数据库读写是否分离 主从式有效 diff --git a/Server/config/middleware.php b/Server/config/middleware.php index fe15ec3d..2edfee13 100755 --- a/Server/config/middleware.php +++ b/Server/config/middleware.php @@ -15,4 +15,12 @@ return [ // 默认中间件命名空间 'default_namespace' => 'app\\http\\middleware\\', + + // 别名或分组 + 'alias' => [ + 'jwt' => 'JwtAuth', + ], + + // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 + 'priority' => [], ]; diff --git a/Server/public/.htaccess b/Server/public/.htaccess index f4eba086..e69de29b 100755 --- a/Server/public/.htaccess +++ b/Server/public/.htaccess @@ -1,8 +0,0 @@ - - Options +FollowSymlinks -Multiviews - RewriteEngine On - - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^(.*)$ index.php [L] - diff --git a/Server/public/api_test.php b/Server/public/api_test.php new file mode 100644 index 00000000..9f5ccb7d --- /dev/null +++ b/Server/public/api_test.php @@ -0,0 +1,76 @@ +API测试工具'; +echo '
'; +echo '

API路径:

'; +echo '

请求方法:

'; +echo '

请求数据 (JSON):

'; +echo '

Authorization Token:

'; +echo '

'; +echo '
'; + +// 如果有URL参数,发送API请求 +if (!empty($url)) { + // 构建完整URL + $fullUrl = 'http://' . $_SERVER['HTTP_HOST'] . '/' . $url; + + // 初始化cURL + $ch = curl_init($fullUrl); + + // 设置cURL选项 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // 设置请求方法 + if ($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($jsonData)); + } + + // 设置请求头 + $headers = ['Content-Type: application/json']; + if (!empty($token)) { + $headers[] = 'Authorization: ' . $token; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // 执行请求 + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // 检查是否有错误 + if (curl_errno($ch)) { + echo '

请求错误

'; + echo '
' . htmlspecialchars(curl_error($ch)) . '
'; + } else { + echo '

响应结果 (HTTP状态码: ' . $httpCode . ')

'; + echo '
' . htmlspecialchars($result) . '
'; + + // 尝试解析JSON + $jsonResult = json_decode($result, true); + if (json_last_error() === JSON_ERROR_NONE) { + echo '

格式化JSON响应

'; + echo '
' . htmlspecialchars(json_encode($jsonResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . '
'; + } + } + + // 关闭cURL资源 + curl_close($ch); +} \ No newline at end of file diff --git a/Server/public/login.html b/Server/public/login.html new file mode 100644 index 00000000..fabec674 --- /dev/null +++ b/Server/public/login.html @@ -0,0 +1,123 @@ + + + + + + 登录测试 + + + +

JWT登录测试

+ +
+ + +
+ +
+ + +
+ + + + +
+

响应结果将显示在这里

+
+ + + + \ No newline at end of file diff --git a/Server/public/nginx.htaccess b/Server/public/nginx.htaccess new file mode 100644 index 00000000..250ba186 --- /dev/null +++ b/Server/public/nginx.htaccess @@ -0,0 +1,8 @@ +location ~* (runtime|application)/{ + return 403; +} +location / { + if (!-e $request_filename){ + rewrite ^(.*)$ /index.php?s=$1 last; break; + } +} \ No newline at end of file diff --git a/Server/public/test.php b/Server/public/test.php new file mode 100644 index 00000000..6b9703c8 --- /dev/null +++ b/Server/public/test.php @@ -0,0 +1,36 @@ + 'admin', + 'password' => '123456' +]); + +// 初始化cURL +$ch = curl_init($url); + +// 设置cURL选项 +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, $data); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data) +]); + +// 执行请求 +$result = curl_exec($ch); + +// 检查是否有错误 +if (curl_errno($ch)) { + echo '请求错误: ' . curl_error($ch); +} else { + // 输出结果 + echo '
';
+    print_r(json_decode($result, true));
+    echo '
'; +} + +// 关闭cURL资源 +curl_close($ch); \ No newline at end of file diff --git a/Server/route/route.php b/Server/route/route.php index 6f479d30..2fe062ec 100755 --- a/Server/route/route.php +++ b/Server/route/route.php @@ -9,12 +9,15 @@ // | Author: liu21st // +---------------------------------------------------------------------- +use think\facade\Route; + Route::get('think', function () { return 'hello,ThinkPHP5!'; }); Route::get('hello/:name', 'index/hello'); -return [ +// 加载Common模块路由配置 +include __DIR__ . '/../application/common/config/route.php'; -]; +return [];