本文目的:通过Tp5 + Vue
实现跨域请求,用JWT Token
校验接口,实现一套简单的闭环。
一、什么是JWT?
JSON Web Token(JWT)
是目前最流行的跨域身份验证解决方案。
参考:
JSON Web Token 入门教程
JWT官网
二、逻辑梳理
前端登录成功,后台返回jwt生成的token,前端将token保存本地。
前端每次接口请求,携带token给后台,后台对其进行校验,校验成功做逻辑处理并生成新token返回,本地更新token。
三、开始项目
- 创建vue:
vue init webpack
- 在
src/components
下新建五个组件
Main.vue
<template>
<div>
<router-view></router-view>
<navs></navs>
</div>
</template>
<script>
import Navs from '@/components/Navs.vue'
export default {
components: {
Navs
}
}
</script>
Home.vue
<template>
<div>
<p>首页</p>
</div>
</template>
Category.vue
<template>
<div>
<p>分类</p>
</div>
</template>
Login.vue
<template>
<div>
<p>登录</p>
</div>
</template>
Navs.vue
<template>
<div class="nav">
<ul>
<router-link v-for="(item, index) in navs" :to="{name: item.name}" :key="index" tag="li" exact>{{item.title}}</router-link>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
navs: [
{ title: '首页', name: 'home' },
{ title: '分类', name: 'category' },
{ title: '登录', name: 'login' }
]
}
},
created () {
}
}
</script>
<style>
.nav ul { position: fixed; bottom: 0; width: 100%; height: 50px; line-height: 50px; border-top: 1px solid #F3F3F3;}
.nav li { float: left; display: inline; width: 33.33333%; text-align: center; cursor: pointer;}
.nav li.active { color: red; }
</style>
- 修改
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
* { margin: 0; padding: 0; text-align: center; }
</style>
- 修改
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Main from '@/components/Main'
import Home from '@/components/Home'
import Login from '@/components/Login'
import Category from '@/components/Category'
Vue.use(Router)
export default new Router({
mode: 'history',
linkActiveClass: 'active',
routes: [
{
path: '/',
component: Main,
children: [
{name: 'home', path: '', component: Home},
{name: 'category', path: 'category', component: Category},
{name: 'login', path: 'login', component: Login}
]
}
]
})
-
npm run dev
运行,可以看到简单的页面了
- 安装axios和qs:
npm i axios -D
、npm i qs -D
- 新建
src/axios/index.js
import axios from 'axios'
import qs from 'qs'
axios.defaults.timeout = 5000
axios.defaults.baseURL = ''
// http request 拦截器
axios.interceptors.request.use(
config => {
config.data = qs.stringify(config.data)
config.headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
let token = (() => {
return localStorage.getItem('token')
})()
let url = config.url
// 非登录请求,headers添加token
if (url.indexOf('login') < 0) {
config.headers.Authorization = token
}
// 登录请求,从headers删除token
if (url.indexOf('login') > -1) {
localStorage.removeItem('token')
delete config.headers.Authorization
}
return config
},
error => {
return Promise.reject(error)
}
)
// http response 拦截器
axios.interceptors.response.use(
response => {
// 更新token
if (response.headers.token) {
localStorage.setItem('token', response.headers.token)
}
return response
},
error => {
return Promise.reject(error)
}
)
export default axios
- 在
main.js
中引入axios
...
import Axios from '@/axios'
Vue.prototype.$axios = Axios
...
- 在本地建一个thinkphp5框架的虚拟域名
http://tp5
- tp5安装jwt:
composer require lcobucci/jwt
lcobucci版本 https://github.com/lcobucci/jwt - tp5下新建
application/index/controller/Jwt.php
,写入两个私有方法:generateToken()
和verifyToken
<?php
namespace app\index\controller;
use think\Controller;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Parser;
class Jwt extends Controller{
public $secret = "nbweo2i3nlxnla;3igasldnKWL2";
// 生成令牌
private function generateToken($user_id, $user_name){
$builder = new Builder();
$signer = new Sha256();
$token = $builder->setIssuer("tp5")
->setAudience("localhost:8080")
->setId("abc", true)
->setIssuedAt(time())
->setNotBefore(time() + 60)
->setExpiration(time() + 3600)
->set("user_id", $user_id)
->set("user_name", $user_name)
->sign($signer, $this->secret)
->getToken();
$token = (string)$token;
return $token;
}
// 验证令牌
private function verifyToken($token){
$signer = new Sha256();
if(!$token){
return [
'msg' => "Invalid token",
'status' => 'fail'
];
}
try {
$parse = (new Parser())->parse($token);
if(!$parse->verify($signer, $this->secret)){
return [
'msg' => "Invalid token",
'status' => 'fail'
];
}
if($parse->isExpired()){
return [
'msg' => "Already expired",
'status' => 'fail'
];
}
return $parse;
} catch (\Exception $e) {
return [
'msg' => 'token异常',
'status' => 'fail'
];
}
}
}
- 测试一下jwt令牌
// 测试jwt
public function test () {
return $this->generateToken(1, '北鱼');
}
浏览器访问查看:
- 登录
Login.vue
<template>
<div>
<p>登录</p>
<br>
<p>
<button @click="login()">点击登录</button>
</p>
</div>
</template>
<script>
export default {
methods: {
// 登录
login () {
let token = localStorage.getItem('token')
let data = { name: '北鱼', password: '123456' }
let url = 'http://tp5/index/jwt/login'
if (!token) {
this.$axios.post(url, data).then((res) => {
// 登录成功跳首页
if (res.data.status === 'success') {
this.$router.push({name: 'home'})
}
})
} else {
// 已登录跳首页
this.$router.push({name: 'home'})
}
}
}
}
</script>
相应的后台添加login
方法:
// 登录
// 登录
public function login(){
$user_id = 123;
$user_name = '北鱼';
$token = $this->generateToken($user_id, $user_name);
return json([
'status' => 'success'
], 200, [
'token' => $token
]);
}
跨域访问后台加允许:
public function _initialize(){
// 跨域访问
header('Access-Control-Allow-Origin: http://localhost:8080');
// 也可以添加所有的请求为 允许,当然不推荐这样做了
// header('Access-Control-Allow-Origin: *');
}
-
再次点击登录,headers中有返回,但未接收到token:
后台将token暴露出来,让前端可以取到,在
_initialize
// CORS跨域时axios无法获取服务器自定义的header信息
header('Access-Control-Expose-Headers: token');
-
点击登录,查看,这次本地存储已经有了
- 修改分类页面,携带token并获取数据
- 修改
Category.vue
<template>
<div><br/><br/>
<p>category</p><br/><br/>
<p v-if="create_time">token创建时间:{{ create_time }}</p>
<p v-if="status">status:{{ status }} - {{ msg }}</p><br/><br/>
<ul>
<li v-for="(item, index) in list" :key="index" >{{item.title}} - {{item.desc}}</li>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
list: [],
create_time: '',
status: '',
msg: ''
}
},
created () {
this.getList()
},
methods: {
getList () {
let url = 'http://tp5/index/jwt/getList'
this.$axios.post(url).then((res) => {
this.list = res.data.list
this.create_time = res.data.create_time
this.status = res.data.status
this.msg = res.data.msg
// 如果失败,则清除token
if (res.data.status === 'fail') {
localStorage.removeItem('token')
}
})
}
}
}
</script>
- 后台添加
getList
方法:先校验token,通过后生成新token
public function getList(){
$token = $this->request->header('authorization');
$verify = $this->verifyToken($token);
if(is_array($verify) && $verify['status'] == 'fail'){
return json($verify);
}
$token = $this->generateToken($verify->getClaim('user_id'), $verify->getClaim('user_name'));
return json([
'list' => [
[
'title' => 'title',
'desc' => 'desc'
]
],
'status' => 'success',
'msg' => '获取数据成功',
'create_time' => date("Y-m-d H:i:s", $verify->getClaim("iat"))
], 200, [
'token' => $token
]);
}
-
刷新分类页,查看
- 问题:请求头字段
Authorization
不被允许,后台加允许:
// 设置允许headers: Authorization
header("Access-Control-Allow-Headers: Authorization");
-
刷新分类页,查看数据正常
- 仔细查看网络请求,我们发现刷新分类页面时,
getList
出现了两次请求
解决:vue axios跨域请求发送两次问题 - 后台加判断处理
// CORS跨域,会先发送一次options请求预检,做不处理
if($this->request->method() === 'OPTIONS'){
die;
}
- 修改
Home.vue
<template>
<div class="home">
<p>首页</p>
{{ isLogin ? '已登录' : '未登录'}}
</div>
</template>
<script>
export default {
computed: {
isLogin () {
let token = window.localStorage.getItem('token')
if (token && token !== 'undefined') {
return true
}
return false
}
}
}
</script>
<style>
.home { padding-top: 100px; line-height: 50px; }
</style>
完整Jwt.php
<?php
namespace app\index\controller;
use think\Controller;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Parser;
class Jwt extends Controller{
public $secret = "nbweo2i3nlxnla;3igasldnKWL2";
public function _initialize(){
// 跨域访问
header('Access-Control-Allow-Origin: http://localhost:8080');
// 也可以添加所有的请求为 允许,当然不推荐这样做了
// header('Access-Control-Allow-Origin: *');
// 设置允许headers: Authorization
header("Access-Control-Allow-Headers: Authorization");
// CORS跨域时axios无法获取服务器自定义的header信息
header('Access-Control-Expose-Headers: token,uid');
// CORS跨域,会先发送一次options请求预检,做不处理
if($this->request->method() === 'OPTIONS'){
die;
}
}
// 登录
public function login(){
$user_id = 123;
$user_name = '北鱼';
$token = $this->generateToken($user_id, $user_name);
return json([
'status' => 'success'
], 200, [
'token' => $token
]);
}
// 列表
public function getList(){
$token = $this->request->header('authorization');
$verify = $this->verifyToken($token);
if(is_array($verify) && $verify['status'] == 'fail'){
return json($verify);
}
$token = $this->generateToken($verify->getClaim('user_id'), $verify->getClaim('user_name'));
return json([
'list' => [
[
'title' => 'title',
'desc' => 'desc'
]
],
'status' => 'success',
'msg' => '获取数据成功',
'create_time' => date("Y-m-d H:i:s", $verify->getClaim("iat"))
], 200, [
'token' => $token
]);
}
// 生成令牌
private function generateToken($user_id, $user_name){
$builder = new Builder();
$signer = new Sha256();
$token = $builder->setIssuer("tp5")
->setAudience("localhost:8080")
->setId("abc", true)
->setIssuedAt(time())
->setNotBefore(time() + 60)
->setExpiration(time() + 3600)
->set("user_id", $user_id)
->set("user_name", $user_name)
->sign($signer, $this->secret)
->getToken();
$token = (string)$token;
return $token;
}
// 验证令牌
private function verifyToken($token){
$signer = new Sha256();
if(!$token){
return [
'msg' => "Invalid token",
'status' => 'fail'
];
}
try {
$parse = (new Parser())->parse($token);
if(!$parse->verify($signer, $this->secret)){
return [
'msg' => "Invalid token",
'status' => 'fail'
];
}
if($parse->isExpired()){
return [
'msg' => "Already expired",
'status' => 'fail'
];
}
return $parse;
} catch (\Exception $e) {
return [
'msg' => 'token异常',
'status' => 'fail'
];
}
}
public function test () {
return $this->generateToken(1, '北鱼');
}
}
所有代价在这可以查看:https://github.com/xuyufei/jwt-demo.git
最后:项目很简单,有些细节需要注意。