使用 Laravel Sanctum 构建基于令牌的 Vue.js 客户端 SPA 身份验证

2025-06-10

使用 Laravel Sanctum 构建基于令牌的 Vue.js 客户端 SPA 身份验证

身份验证系统是大多数现代应用程序的重要组成部分,因此应该得到适当实施。

在本文中,您将学习如何使用Vue.jsLaravel Sanctum(以前称为 Airlock)构建身份验证系统。

我们将为前端和后端创建单独的项目,它们将通过 REST API 相互交互。

让我们开始吧!

后端(Laravel)

步骤#1

有关 Laravel 安装说明,请访问官方文档页面。

在终端中运行创建一个新的 Laravel 项目

laravel new my-app
Enter fullscreen mode Exit fullscreen mode

或者

composer create-project --prefer-dist laravel/laravel my-app
Enter fullscreen mode Exit fullscreen mode

我正在使用 Laravel Valet,它自动允许我们在域名上访问我们的网站http://my-app.test

在您的机器上,它将根据您的本地开发环境设置进行访问。

步骤#2

在应用程序目录中创建一个名为的新数据库my-app并设置DB_DATABASE=my-app文件。.env

步骤#3

安装 Laravel Sanctum。

composer require laravel/sanctum
Enter fullscreen mode Exit fullscreen mode

使用 Artisan 命令发布 Sanctum 配置和迁移文件vendor:publishsanctum配置文件将放置在您的config目录中:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Enter fullscreen mode Exit fullscreen mode

运行数据库迁移来创建用于存储 API 令牌的数据库表:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

将 Sanctum 的中间件添加到app/Http/Kernel.phpapi中的中间件组

../app/Http/Kernel.php

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

...

    protected $middlewareGroups = [
        ...

        'api' => [
            EnsureFrontendRequestsAreStateful::class,
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    ...
],
Enter fullscreen mode Exit fullscreen mode

步骤#4

要为用户使用令牌,我们必须将其添加HasApiTokensapp/User.phpUser中的模型中

../app/User.php

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}
Enter fullscreen mode Exit fullscreen mode

步骤#5

让我们为模型创建种子User。稍后我们需要它来测试登录过程。

php artisan make:seeder UsersTableSeeder
Enter fullscreen mode Exit fullscreen mode

现在让我们插入

DB::table('users')->insert([
    'name' => 'John Doe',
    'email' => 'john@doe.com',
    'password' => Hash::make('password')
]);
Enter fullscreen mode Exit fullscreen mode

进入数据库/seeds/UsersTableSeeder.phprun()中的函数

要用用户种子users表,让我们运行:

php artisan db:seed --class=UsersTableSeeder
Enter fullscreen mode Exit fullscreen mode

现在我们的数据库中有一个名为John Doeemailjohn@doe.com和密码的新用户password

步骤#6

让我们在routes/api.php/login文件中创建一个路由

../routes/api.php

use App\User;
use Illuminate\Support\Facades\Hash;

Route::post('/login', function (Request $request) {
    $data = $request->validate([
        'email' => 'required|email',
        'password' => 'required'
    ]);

    $user = User::where('email', $request->email)->first();

    if (!$user || !Hash::check($request->password, $user->password)) {
        return response([
            'message' => ['These credentials do not match our records.']
        ], 404);
    }

    $token = $user->createToken('my-app-token')->plainTextToken;

    $response = [
        'user' => $user,
        'token' => $token
    ];

    return response($response, 201);
});
Enter fullscreen mode Exit fullscreen mode

步骤#7

让我们向路由发送一个包含邮箱john@doe.com和密码password参数的POST 请求http://my-app.test/api/login。您可以使用PostmanInsomnia软件包来完成此操作。

如果一切顺利,我们将收到一个 JSON 对象作为对我们请求的响应:

{
    "user": {
        "id": 1,
        "name": "John Doe",
        "email": "john@doe.com",
        "email_verified_at": null,
        "created_at": null,
        "updated_at": null
    },
    "token": "AbQzDgXa..."
}
Enter fullscreen mode Exit fullscreen mode

步骤#8

接下来,我们需要修改一些中间件。我们在/routes/api.php文件中将以下内容替换auth:apiauth:sanctum

../routes/api.php

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});
Enter fullscreen mode Exit fullscreen mode

步骤#9

在我们继续前端之前,我们必须设置跨域请求CORS处理。

../config/cors.php

    'paths' => ['api/*', 'login', 'logout'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,
Enter fullscreen mode Exit fullscreen mode
../.env

SANCTUM_STATEFUL_DOMAINS=127.0.0.1
Enter fullscreen mode Exit fullscreen mode

前端(Vue.js)

我们将使用Vuex进行状态管理,使用Vue Router进行路由,并使用axios发出 HTTP 请求。

步骤#1

我们将使用Vue CLI创建一个新的 Vue 项目。如果您不熟悉这个 Vue.js 开发的标准工具,请阅读本指南

在我们用于项目的目录中,运行以下命令:

vue create my-vue-app
Enter fullscreen mode Exit fullscreen mode

选择Manually select features然后选择RouterVuex

vue_create

成功创建my-vue-app项目后,运行以下命令:

cd my-vue-app
npm run serve
Enter fullscreen mode Exit fullscreen mode

现在我们的应用程序应该可以在http://localhost:8080/域中使用了。

步骤#2

让我们为Login视图创建一个新文件。

..src/views/Login.vue

<template>
  <div>
    <h1>Login</h1>
    <form @submit.prevent="login">
      <input type="email" name="email" v-model="email">
      <input type="password" name="password" v-model="password">
      <button type="submit">Login</button>
    </form>
  </div>
</template>

<script>
export default {
  data () {
    return {
      email: '',
      password: ''
    }
  },

  methods: {
    login () {
      this.$store
        .dispatch('login', {
          email: this.email,
          password: this.password
        })
        .then(() => {
          this.$router.push({ name: 'About' })
        })
        .catch(err => {
          console.log(err)
        })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

在中Vue Router,我们必须为Login视图实现一条路线。

../src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Enter fullscreen mode Exit fullscreen mode

现在,如果我们在浏览器中导航http://localhost:8080/login,我们可以看到登录页面。

登录

步骤#3

我们必须在前端目录中安装axios才能发出 HTTP 请求:

npm install axios
Enter fullscreen mode Exit fullscreen mode

步骤#4

让我们(login/logout)在 Vuex 中实现一些用户身份验证操作。

../src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

axios.defaults.baseURL = 'http://app-backend.test/api'

export default new Vuex.Store({
  state: {
    user: null
  },

  mutations: {
    setUserData (state, userData) {
      state.user = userData
      localStorage.setItem('user', JSON.stringify(userData))
      axios.defaults.headers.common.Authorization = `Bearer ${userData.token}`
    },

    clearUserData () {
      localStorage.removeItem('user')
      location.reload()
    }
  },

  actions: {
    login ({ commit }, credentials) {
      return axios
        .post('/login', credentials)
        .then(({ data }) => {
          commit('setUserData', data)
        })
    },

    logout ({ commit }) {
      commit('clearUserData')
    }
  },

  getters : {
    isLogged: state => !!state.user
  }
})

Enter fullscreen mode Exit fullscreen mode

成功登录后,我们将在user变量和中存储一些用户数据localStorage

步骤#5

让我们为经过身份验证和未经身份验证的页面定义路由。

我们可以使About页面仅供经过身份验证的用户访问。

为了这个目的,我们将该meta字段添加到About路线中。

让我们使用 Vue Router 的beforeEach方法来检查用户是否已登录。如果用户未通过身份验证,我们将把他们重定向回登录页面。

../src/router.index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    meta: {
      auth: true
    },
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user')

  if (to.matched.some(record => record.meta.auth) && !loggedIn) {
    next('/login')
    return
  }
  next()
})

export default router

Enter fullscreen mode Exit fullscreen mode

步骤#6

如果用户刷新页面怎么办?我们应该要求他重新登录吗?

当然不是!

让我们created()向实例添加一个方法Vue来处理该场景。

created () {
  const userInfo = localStorage.getItem('user')
  if (userInfo) {
    const userData = JSON.parse(userInfo)
    this.$store.commit('setUserData', userData)
  }
}
Enter fullscreen mode Exit fullscreen mode

步骤#7

我们还需要处理令牌过期或用户未经授权的情况。

让我们在created()方法中使用拦截器来实现这一点。

因此我们更新后的main.js文件如下所示:

../src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  created () {
    const userInfo = localStorage.getItem('user')
    if (userInfo) {
      const userData = JSON.parse(userInfo)
      this.$store.commit('setUserData', userData)
    }
    axios.interceptors.response.use(
      response => response,
      error => {
        if (error.response.status === 401) {
          this.$store.dispatch('logout')
        }
        return Promise.reject(error)
      }
    )
  },
  render: h => h(App)
}).$mount('#app')

Enter fullscreen mode Exit fullscreen mode

步骤#8

我们还没有实现某个Logout功能。让我们在App.vue文件中实现它。

此外,我们仅在用户登录时显示About和按钮。Logout

../src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about" v-if="isLogged">About</router-link>
      <router-link to="/login" v-else>Login</router-link>
      <button type="button" @click="logout()" v-if="isLogged">
        Logout
      </button>
    </div>
    <router-view/>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      'isLogged'
    ])
  },

  methods: {
    logout () {
      this.$store.dispatch('logout')
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

好的,我们的教程已经完成了。

我希望这些信息对您有帮助!

查看源代码
前端
后端

鏂囩珷鏉ユ簮锛�https://dev.to/romanpaprotsky/vue-js-token-based-authentication-with-laravel-sanctum-3a84
PREV
更多表单动画实验
NEXT
React 上下文、性能?