JWT:基于Token的WEB后台认证机制

本文目的:通过Tp5 + Vue实现跨域请求,用JWT Token校验接口,实现一套简单的闭环。

一、什么是JWT?

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。
参考:
JSON Web Token 入门教程
JWT官网

二、逻辑梳理

前端登录成功,后台返回jwt生成的token,前端将token保存本地。
前端每次接口请求,携带token给后台,后台对其进行校验,校验成功做逻辑处理并生成新token返回,本地更新token。


逻辑梳理

三、开始项目

  1. 创建vue:vue init webpack
  2. 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>

  1. 修改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>

  1. 修改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}
      ]
    }
  ]
})
  1. npm run dev运行,可以看到简单的页面了
    npm run dev
  2. 安装axios和qs:npm i axios -Dnpm i qs -D
  3. 新建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
  1. main.js中引入axios
...
import Axios from '@/axios'
Vue.prototype.$axios = Axios
...
  1. 在本地建一个thinkphp5框架的虚拟域名http://tp5
  2. tp5安装jwt:composer require lcobucci/jwt
    lcobucci版本 https://github.com/lcobucci/jwt
  3. 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'
            ];
        }
    }
    
}
  1. 测试一下jwt令牌
    // 测试jwt
    public function test () {
        return $this->generateToken(1, '北鱼');
    }

浏览器访问查看:


生成令牌
  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
        ]);
    }
跨域访问:access-control-allow-origin报错
跨域访问后台加允许:
    public function _initialize(){
        // 跨域访问
        header('Access-Control-Allow-Origin: http://localhost:8080');
        
        // 也可以添加所有的请求为 允许,当然不推荐这样做了
        // header('Access-Control-Allow-Origin: *');
    }
  • 再次点击登录,headers中有返回,但未接收到token:


    token返回

    前端未接收到token
  • 后台将token暴露出来,让前端可以取到,在_initialize

        // CORS跨域时axios无法获取服务器自定义的header信息
        header('Access-Control-Expose-Headers: token');
  • 点击登录,查看,这次本地存储已经有了


    token
  1. 修改分类页面,携带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
        ]);
    }
  • 刷新分类页,查看


    Request header field Authorization is not allowed
  • 问题:请求头字段Authorization不被允许,后台加允许:
        // 设置允许headers: Authorization
        header("Access-Control-Allow-Headers: Authorization");
  • 刷新分类页,查看数据正常


    数据正常
  • 仔细查看网络请求,我们发现刷新分类页面时,getList出现了两次请求
    两次请求

    解决:vue axios跨域请求发送两次问题
  • 后台加判断处理
        // CORS跨域,会先发送一次options请求预检,做不处理
        if($this->request->method() === 'OPTIONS'){
            die;
        }
  1. 修改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>
Home.vue
完整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

最后:项目很简单,有些细节需要注意。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容