Vue3 + ThinkPHP6 JWT 完整教程(前后端分离登录鉴权)
整体流程
- 前端提交账号密码 → TP6后端校验账号密码
- 校验成功,后端生成JWT令牌返回前端
- 前端保存token,axios请求拦截器自动在请求头带上
Authorization: Bearer token - 后端JWT中间件校验token,合法放行接口;失效/错误返回
401 - 前端响应拦截捕获401,清除token、跳转到登录页;路由守卫阻止未登录访问需要权限的页面
两种方案:① 原生
firebase/jwt手写JWT(理解原理);②thans/tp-jwt-auth封装库(项目推荐),下面同时给出。
一、ThinkPHP6 后端部分
1. 安装依赖
方案A:官方推荐扩展 thans/tp-jwt-auth(推荐项目使用)
bash
composer require thans/tp-jwt-auth
复制配置文件:
php think jwt:publish
修改 .env 配置 jwt 密钥、过期时间
ini
JWT_SECRET=你的超级密钥,尽量长,不要泄露
JWT_TTL=7200 # token有效期 单位秒 2小时
JWT_REFRESH_TTL=20160 #刷新有效期 14天
方案B:原生 firebase/jwt(学习原理)
bash
composer require firebase/php-jwt
2. 登录控制器(生成Token)
路由:
POST /api/login
php
<?php
namespace app\controller;
use think\Request;
use thans\jwt\facade\JWTAuth;
class User
{
//登录接口
public function login(Request $request)
{
$username = $request->post('username');
$password = $request->post('password');
// 查询用户(自行替换user表,密码建议password_hash哈希存储)
$user = \app\model\User::where('username',$username)->find();
if(!$user || !password_verify($password,$user['password'])){
return json(['code'=>400,'msg'=>'账号或密码错误']);
}
//生成token,payload写入uid等信息
$token = JWTAuth::builder(['uid'=>$user['id'],'username'=>$user['username']]);
return json([
'code'=>200,
'msg'=>'登录成功',
'data'=>[
'token'=>$token
]
]);
}
}
3. 创建JWT鉴权中间件(thans扩展自带中间件)
app/middleware.php 注册中间件
kotlin
return [
\thans\jwt\middleware\JWTAuth::class
];
路由定义 route/api.php
php
<?php
use think\facade\Route;
//登录不需要鉴权
Route::post('login','User/login');
//需要token鉴权的接口,加jwt中间件
Route::group(function(){
Route::get('info','User/info'); //获取用户信息接口
})->middleware(\thans\jwt\middleware\JWTAuth::class);
原生 firebase/jwt 手写中间件(了解原理)
app/middleware/CheckJwt.php
php
<?php
namespace app\middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use think\Request;
class CheckJwt
{
public function handle(Request $request, \Closure $next)
{
$header = $request->header('authorization');
if(!$header){
return json(['code'=>401,'msg'=>'缺少token'],401);
}
$token = str_replace('Bearer ','',$header);
$key = '你的密钥';
try{
$decoded = JWT::decode($token,new Key($key,'HS256'));
//把解析出来的uid挂载到request
$request->uid = $decoded->uid;
}catch(\Exception $e){
return json(['code'=>401,'msg'=>'token无效或已过期'],401);
}
return $next($request);
}
}
4. 获取用户信息接口(需要鉴权)
php
public function info()
{
$payload = JWTAuth::auth();
$uid = $payload['uid'];
$user = \app\model\User::find($uid);
return json(['code'=>200,'data'=>$user]);
}
跨域处理:TP6建议安装
composer require topthink/think-cors解决前端axios跨域
二、Vue3前端部分(Vite + axios + Pinia + VueRouter)
1. 安装包
npm install axios pinia vue-router
2. axios封装 src/utils/request.js
javascript
import axios from 'axios'
const service = axios.create({
baseURL: 'http://tp6-api域名/api',
timeout: 10000
})
// 请求拦截器:每次请求自动带上token
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if(token){
config.headers.Authorization = `Bearer ${token}`
}
return config
},err=>Promise.reject(err))
//响应拦截器:捕获401,token失效
service.interceptors.response.use(res=>{
return res.data
},err=>{
if(err.response && err.response.status ===401){
localStorage.removeItem('token')
//跳转到登录页
import('@/router').then(({default:router})=>{
router.push('/login')
})
}
return Promise.reject(err)
})
export default service
3. API封装 src/api/user.js
javascript
import request from '@/utils/request'
//登录
export function login(data){
return request.post('/login',data)
}
//获取用户信息
export function getUserInfo(){
return request.get('/info')
}
4. Pinia 状态管理 src/stores/user.js(保存登录状态)
javascript
import {defineStore} from 'pinia'
export const useUserStore = defineStore('user',{
state:()=>({
token:localStorage.getItem('token') || '',
userInfo:{}
}),
actions:{
setToken(val){
this.token = val
localStorage.setItem('token',val)
},
logout(){
this.token=''
this.userInfo={}
localStorage.removeItem('token')
}
}
})
5. 路由守卫 src/router/index.js
javascript
import {createRouter,createWebHistory} from 'vue-router'
import {useUserStore} from '@/stores/user'
const routes = [
{path:'/login',component:()=>import('@/views/login.vue')},
{path:'/',component:()=>import('@/views/home.vue'),meta:{requireAuth:true}}
]
const router = createRouter({
history:createWebHistory(),
routes
})
//全局前置守卫
router.beforeEach((to,from,next)=>{
const userStore = useUserStore()
if(to.meta.requireAuth && !userStore.token){
next('/login')
}else{
next()
}
})
export default router
6. 登录页面 views/login.vue
xml
<template>
<div>
<input v-model="form.username" placeholder="账号"/>
<input v-model="form.password" type="password" placeholder="密码"/>
<button @click="handleLogin">登录</button>
</div>
</template>
<script setup>
import {reactive} from 'vue'
import {useRouter} from 'vue-router'
import {login} from '@/api/user'
import {useUserStore} from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
const form = reactive({username:'',password:''})
const handleLogin = async ()=>{
const res = await login(form)
if(res.code === 200){
userStore.setToken(res.data.token)
router.push('/')
}
}
</script>
三、常见坑
- Authorization 大小写 :后端读取
Authorization,不要写成小写authorization,nginx有时候会丢header,需要配置。 - Bearer后面有空格 :
Bearer xxxx空格不能丢,后端解析会失败。 - 跨域:OPTIONS预检请求,TP6必须配置cors,否则header带不上token。
- JWT密钥:生产环境密钥不要硬写代码,放到.env,不要提交到git。
- 退出登录:JWT本身无状态,单纯前端清token;如果需要强制失效,要用Redis黑名单。
- token刷新:thans/jwt-auth自带refreshToken,适合长登录场景。
安全建议
- 密码入库必须
password_hash加密,禁止明文 - 生产环境接口使用HTTPS
- payload不要存放敏感数据(JWT可以前端解码)
- 重要接口增加Redis黑名单实现token主动失效