Laravel 教程:使用 Laravel Sanctum 作为 API 认证来构建 Vue.js 应用

Laravel

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

在本文中,您将学习到如何使用 Vue.js 和 Laravel Sanctum (以前的 Airlock )构建身份验证系统。

我们会创建一个前后端分离的项目,前后端将通过 REST API 相互交互。

让我开始吧!

后端 (Laravel)

第 1 步

有关 Laravel 安装说明,请移步 官方文档 。

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

laravel new my-app

或者

composer create-project --prefer-dist laravel/laravel my-app

我正在使用 Laravel Valet,它会自动将应用创建在 http://my-app.test 域名下。

你可以根据你本地的开发环境来配置并访问它。

第 2 步

创建一个名为 my-app 的数据库,并在 .env 文件中设置数据库连接,DB_DATABASE=my-app

第 3 步

安装 Laravel Sanctum

composer require laravel/sanctum

使用 vendor:publish 命令发布 Sanctum 配置文件和数据库迁移文件。sanctum 配置文件会创建在 config 目录中。

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

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

php artisan migrate

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

../app/Http/Kernel.php

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

...

    protected $middlewareGroups = [
        ...

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

    ...
],

第 4 步

要为用户使用令牌,我们必须在 app/User.phpUser 模型中添加 HasApiTokens

../app/User.php

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

第5步 #5

让我们为 User 模型创建一个 Seeder。我们稍后会需要它来测试登录过程。

php artisan make:seeder UsersTableSeeder

现在,让我们在database/seeds/UsersTableSeeder.phprun()方法中,插入以下代码:

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

给 users 表生成 user, 让我们运行:

php artisan db:seed --class=UsersTableSeeder

现在我们的数据库里有一个新用户叫 "John Doe",邮箱是 "john@doe.com",密码是 "password"。

第6步 #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);
});

第7步 #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..."
}

第8步 #8

接下来,我们需要改变一些中间件。我们在/routes/api.php文件中把auth:api替换成auth:sanctum

../routes/api.php

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

第9步 #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,
../.env

SANCTUM_STATEFUL_DOMAINS=127.0.0.1

前端 (Vue.js)

我们将使用 Vuex来进行状态管理,  用Vue Router 进行路由以及 axios 来进行HTTP请求

步骤 #1

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

在我们项目的目录中,让我们运行以下命令

vue create my-vue-app

选择  Manually select features ,然后选择Router 和 Vuex

Laravel

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

cd my-vue-app
npm run serve

现在我们的应用程序应该可以在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>

在 "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',
    // 路由级别代码分割
    // 这会为这个路由生成一个单独的块(about.[hash].js)。
    // 当路由被访问的时候进行懒加载.
    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

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

Laravel

步骤 #3

我们必须在前端目录下安装axios来进行HTTP请求:

npm install axios

步骤 #4

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

../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
  }
})

登录成功后,我们要在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

步骤 #6

如果用户刷新了一个页面怎么办?要不要让他重新登录?

当然不是!

让我们在 Vue 实例中添加一个 created()方法来处理这种情况。

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

步骤 #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')

步骤 #8

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

另外,让我们只在用户登录时显示AboutLogout按钮。

../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>

好了,我们的教程结束了。

希望这些信息对你有所帮助!

查看源码

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/romanpaprotsky/vue-js-tok...

译文地址:https://learnku.com/laravel/t/43077

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 18

file 有人登录的时候遇到过这种情况吗?Postman能正常返回,各种百度找不到解决办法。求大咖帮忙···

3年前 评论

令牌永不过期吗

3年前 评论

@海了个螺 过期时间再配置文件里面配置

3年前 评论

这个库虽然说是官方做的,但是感觉质量一般般。比如token不支持自定义过期时间,只能设置全局的;token过期后依然保存数据库没有删除;把数据库ID直接保存在token当中,会被人猜测出业务量等等;

3年前 评论

@大张 没看懂,到底是能配置还是不能配置啊,如果不能配置就只有写个定时任务去清除token来实现过期了,还有你说的把显示在token中,token里面原本是没有id的只是楼主把用户信息直接这样返回了,你要是不想返回这数据可以不用返回啊,这只是楼主的写法而已

3年前 评论

@海了个螺

  1. 能配置,在 sanctum 的配置文件里面,有一个配置项是 expiration 就是配置 token 的过期时间,但是配置的是全部 token 过期时间,不能针对某一个 token 设置不同的过期时间

  2. token 里面就是已经把 token id 明文显示在里面,这个是生成功 token 的源码:

    public function createToken(string $name, array $abilities = ['*'])
     {
         $token = $this->tokens()->create([
             'name' => $name,
             'token' => hash('sha256', $plainTextToken = Str::random(80)),
             'abilities' => $abilities,
         ]);
    
         return new NewAccessToken($token, $token->id.'|'.$plainTextToken);
     }
3年前 评论

@大张 确实是哦,如果删掉id不会出错,那确实不知道为什么要加上id

3年前 评论

@海了个螺 源码在这里:

public static function findToken($token)
    {
        if (strpos($token, '|') === false) {
            return static::where('token', hash('sha256', $token))->first();
        }

        [$id, $token] = explode('|', $token, 2);

        if ($instance = static::find($id)) {
            return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null;
        }
    }

会先去判断是否带有|,如果有的话就通过ID查询,然后验证token,如果没有的话就直接通过token查看,这个应该是为了兼容旧版本,之前旧版本出现过一次很严重的bug,语法错误

3年前 评论

请问之后的验证操作,就是判断前端请求接口时,请求头中是否携带了 token,如果没有携带就返回,如果携带就查询数据库对应用户?

3年前 评论

如果是多用户登录呢 guard 该如何设置

3年前 评论
xiaochenniao 3年前

如何设置:middleware('auth:sanctum') 未验证返回指定json信息?因为app调用

3年前 评论

关键是,这个既然是jwt的思路,为什么还有session,这个session可以去掉吗?

3年前 评论

如果是api请求的时候请求的时候要带上这个头部 Accept: application/json

curl -H "Accept: application/json" http://localhost:8000/api/user

如果token验证失败的时候就会返回这个

{
    "message": "Unauthenticated."
}
1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!