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

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

一、什么是JWT?

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

二、逻辑梳理

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


6515740-8c861b50700b9cd7.png
逻辑梳理

三、开始项目

  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运行,可以看到简单的页面了
    6515740-737812dc6393b81b.png
    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, '北鱼');
    }

浏览器访问查看:


6515740-212cf82fe566da9f.png
生成令牌
  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>
6515740-e90b3a458801789c.png
加登录按钮

相应的后台添加login方法:

    // 登录
        // 登录
    public function login(){
        $user_id   = 123;
        $user_name = '北鱼';
        
        $token = $this->generateToken($user_id, $user_name);
        
        return json([
            'status' => 'success'
        ], 200, [
            'token' => $token
        ]);
    }
6515740-c988d36710a798b1.png
跨域访问:access-control-allow-origin报错
跨域访问后台加允许:
    public function _initialize(){
        // 跨域访问
        header('Access-Control-Allow-Origin: http://localhost:8080');
        
        // 也可以添加所有的请求为 允许,当然不推荐这样做了
        // header('Access-Control-Allow-Origin: *');
    }
  • 再次点击登录,headers中有返回,但未接收到token:


    6515740-d539462ba3a1c8ac.png
    token返回

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

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


    6515740-4a2aba1a609452f4.png
    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
        ]);
    }
  • 刷新分类页,查看


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


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

    解决: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>
6515740-7e44beda568fab9a.png
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

扫描二维码关注公众号,回复: 5236872 查看本文章

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

猜你喜欢

转载自blog.csdn.net/weixin_33804582/article/details/86845100