thinkphp6:前后端分离使用表单令牌(php 8.1.1 / thinkphp v6.0.10LTS/vue 3.2.26)


一,vue前端代码

1,Login.vue
<template>
  <div style="padding:20px;display: flex;align-items:center;justify-content: center;">
    <form :model="account"  style="margin-top:50px;width:400px;">
        <input v-model="account.username" placeholder="请输入用户名" style="width:392px;font-size:16px;" /><br/>
        <input v-model="account.password"  type="password" placeholder="请输入密码" style="width:392px;margin-top:10px;font-size:16px;" /><br/>
        
        <div    @click="login"  style="margin-top:10px;width: 100%;height:40px;line-height:40px;background: #ff0000;border-radius: 10px;">
          登录
        div>
      <div    @click="info"  style="margin-top:10px;width: 100%;height:40px;line-height:40px;background: #ff0000;border-radius: 10px;">
        info
      div>
        <div class="text-align-right">
        div>
    form>
  div>
template>
<script>
import { ref,  reactive } from "vue";
import { ElMessage } from "element-plus";
import { apiLogin,apiInfo,apiToken} from '@/api/api';
export default {
  name: "Login",
  setup() {
    const accountRef = ref(null);
    //表单字段
    const account = reactive({
      username: "",
      password: "",
    });
    const formToken = ref("");
    const getToken = () => {
      apiToken().then(res => {
        //成功
        if (res.code == 0) {
          formToken.value = res.data.token;
        } else {
          ElMessage.error("获取表单令牌失败:"+res.msg);
        }
      }).catch((error) => {
        console.log(error)
      })
    }
    getToken();

    //登录
    const login = async () => {
      console.log('begin login');
      var data = new FormData();
      data.append("username",account.username);
      data.append("password",account.password);
      data.append("X-CSRF-TOKEN",formToken.value);

          apiLogin(data).then(res => {
            //成功
            if (res.code == 0) {
              //保存jwt token到本地
              localStorage.setItem('token', res.data.token);
              //提示
              ElMessage.success("登录成功!");

            } else {
              ElMessage.error("登录失败:"+res.msg);
            }

          }).catch((error) => {
            console.log(error)
          })
    };

    const login2 = () => {
      console.log('begin login2');
    }

    const info = () => {
      apiInfo().then(res => {
        //成功
        if (res.code == 0) {
           console.log(res.data);
        } else {
          ElMessage.error("用户信息获取失败:"+res.msg);
        }

      }).catch((error) => {
        console.log(error)
      })
    }

    return {
      account,
      accountRef,
      login,
      login2,
      info,
    };
  },
}
script>

<style scoped>
style>
2,api/axios.js
...
//post
export function postForm (url, params) {
  return new Promise((resolve, reject) => {
      let headers = {
          'Content-Type': 'application/x-www-form-urlencoded',
      }
      //判断params中是否包含X-CSRF-TOKEN
      var token = "";
      if (params.has("X-CSRF-TOKEN")) {
          token = params.get("X-CSRF-TOKEN");
          params.delete("X-CSRF-TOKEN");
          headers = {
              'Content-Type': 'application/x-www-form-urlencoded',
              'X-CSRF-TOKEN': token,
          }
      }

    _axios.post(url, params,{headers})
        .then(res => {
          resolve(res.data)
        })
        .catch(err => {
            console.log("api error:");
            console.log(err);
            //alert(err);
          reject(err.data)
        })
  })
}
...

3,api/api.js

//get,
import { get, postForm} from './axios'

//user
export const apiLogin = pLogin => postForm('/api/auth/login', pLogin)
...

说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

         对应的源码可以访问这里获取: https://github.com/liuhongdi/
         或: https://gitee.com/liuhongdi

说明:作者:刘宏缔 邮箱: 371125307@qq.com

二,服务端代码:

1,controller/Auth.php  
<?php
declare (strict_types = 1);

namespace app\controller;

use app\BaseController;
use think\facade\Cache;
use think\Request;
use app\result\Result;
use think\response\Json;
use app\validate\Login as LoginValidate;
use app\validate\GoodsList as GoodsListValidate;
use think\exception\ValidateException;
use app\lib\util\JwtUtil;

class Auth extends BaseController
{
       /*
     *得到form token
     *
     *@return \think\Response
     * */
    public function token():Json {
        $type="sha1";
        $type  = is_callable($type) ? $type : 'md5';
        $token = call_user_func($type, $this->request->server('REQUEST_TIME_FLOAT'));
        $key = uniqid();
        Cache::set($key,$token,300);
        $res = [
            "token"=>$key.$token,
            ];
        return Result::Success($res);
    }

    /**
     * 登录
     *
     * @return \think\Response
     */
    public function login():Json {
        // Header验证
        if ($this->request->header('X-CSRF-TOKEN')){
            $reqToken = $this->request->header('X-CSRF-TOKEN');
            $key = substr($reqToken,0,13);
            $value = Cache::get($key);
            $origToken = $key.$value;
            if ($origToken === $reqToken) {
                //验证通过
            } else {
                //验证不通过
                return Result::Error(422,'表单token错误');
            }
        } else {
            return Result::Error(423,'找不到表单token');
        }
        try {
            validate(LoginValidate::class)
                ->check($_POST);
        } catch (ValidateException $e) {
                // 验证失败 输出错误信息
                return Result::Error(422,$e->getError());
        }
        if ($_POST["username"] == "dddddd" && $_POST["password"] == "111111"){
            //验证成功,生成jwt返回
            $userId = 123;
            $jUtil = new JwtUtil();
            $token = $jUtil->createJwt($userId);
            $res = ["token"=>$token];
            // 防止重复提交
            Cache::delete($key);
            return Result::Success($res);
        } else {
            return Result::Error(422,"用户名密码错误");
        }
    }
} 
2,Result.php  
<?php

namespace app\result;
use think\response\Json;
class Result {
    //success
    static public function Success($data):Json {
        $rs = [
            'code'=>0,
            'msg'=>"success",
            'data'=>$data,
        ];
        return json($rs);
    }
    //error
    static public function Error($code,$msg):Json {
        $rs = [
            'code'=>$code,
            'msg'=>$msg,
            'data'=>"",
        ];
        return json($rs);
    }
}

三,测试效果:

1,界面 2,  提交与返回:

四,查看vue的版本

liuhongdi@lhdpc:/data/vue/demo1$ npm list vue
demo1@0.1.0 /data/vue/demo1
├─┬ @vue/cli-plugin-babel@4.5.15
│ └─┬ @vue/babel-preset-app@4.5.15
│   └── vue@3.2.26 deduped
├─┬ element-plus@1.2.0-beta.6
│ ├─┬ @element-plus/icons-vue@0.2.4
│ │ └── vue@3.2.26 deduped
│ ├─┬ @vueuse/core@7.4.1
│ │ ├─┬ @vueuse/shared@7.4.1
│ │ │ └── vue@3.2.26 deduped
│ │ ├─┬ vue-demi@0.12.1
│ │ │ └── vue@3.2.26 deduped
│ │ └── vue@3.2.26 deduped
│ └── vue@3.2.26 deduped
└─┬ vue@3.2.26
  └─┬ @vue/server-renderer@3.2.26
    └── vue@3.2.26 deduped 

五,查看php和thinkphp的版本: 

php:
liuhongdi@lhdpc:/data/php/admapi$ php --version
PHP 8.1.1 (cli) (built: Dec 20 2021 16:12:16) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.1, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.1, Copyright (c), by Zend Technologies 
thinkphp:
liuhongdi@lhdpc:/var/www/html$ cd /data/php/admapi/
liuhongdi@lhdpc:/data/php/admapi$ php think version
v6.0.10LTS