全栈项目?那你说说你的token怎么实现的吧

前言

经常我们会被面试官问,账号密码被修改了怎么办,这就涉及到token了。假设我在淘宝网站登录了账号进入首页,然后我在另一个设备上同样登录该账号,并且将密码修改,原设备刷新数据会被退出登录,这个过程的实现就需要借助tokentoken翻译过来是令牌的意思

token的实现一定是需要你有登录页面,其他页面,以及后端,本文有点长,因为是手把手教,小白可以放心食用

准备工作

前端简单写两个页面,一个登录页,一个首页

前端

less 复制代码
npm create vite@latest client -- --template vue
cd client
npm i 
npm i vue-router@4 
npm i vant  // 安装vant
npm i axios 

路由

javascript 复制代码
import { createWebHistory, createRouter } from 'vue-router'

const routes = [
    {
        path: '/login',
        component: () => import('../views/Login.vue')
    },
    {
        path: '/home',
        component: () => import('../views/Home.vue') 
    }
]

const router = createRouter({
    history: createWebHistory(),
    routes: routes
})

export default router

登录页

借助vantUI,用他的按钮和输入框

自行引入样式~

ini 复制代码
<template>
    <div>
        <van-form @submit="onSubmit">
            <van-cell-group inset>
                <van-field v-model="username" name="username" label="用户名" placeholder="用户名"
                    :rules="[{ required: true, message: '请填写用户名' }]" />
                <van-field v-model="password" type="password" name="password" label="密码" placeholder="密码"
                    :rules="[{ required: true, message: '请填写密码' }]" />
            </van-cell-group>
            <div style="margin: 16px;">
                <van-button round block type="primary" native-type="submit">
                    登录
                </van-button>
            </div>
        </van-form>
    </div>
</template>

axios发请求

xml 复制代码
<script setup>
import axios from 'axios'
import { ref } from 'vue';

const username = ref('')
const password = ref('') 

const onSubmit = (values) => {
    axios.post('http://localhost:3000/login', values) // 希望后端有这么个地址,并向他传参 一定是个对象,刚好vant就是给你一个对象
    .then(res => {
        console.log(res);
    })
}
</script>

后端

less 复制代码
npm init -y
npm i koa
npm i koa2-cors // koa解决跨域
npm i koa-bodyparser // koa解析不出post请求
npm i koa-router // koa-router写接口

app.js如下

php 复制代码
const Koa = require('koa')
const cors = require('koa2-cors') // 这样不用写相应头,白名单什么的 npm i koa2-cors
const bodyParser= require('koa-bodyparser')
const userRouter = require('./routes/user') // 让接口生效
const app = new Koa()

app.use(bodyParser())// koa无法解析post参数,这是koa一个小问题,安装npm i koa-bodyparser
app.use(cors()) // 处理跨域
app.use(userRouter.routes(), userRouter.allowedMethods()) 

app.listen(3000, () => {
    console.log('项目已启动在在3000端口'); 
})

routes/user.js如下

javascript 复制代码
// 和用户相关的接口
const router = require('koa-router')() // 直接调用

// 定义一个post请求的接口
router.post('/login', (ctx) => {
    let user = ctx.request.body // 拿到前端的参数
    console.log(user)
})

module.exports = router 

好了,现在前端是可以朝着后端发请求的,并且可以拿到数据

好了,上面的准备工作已经完成了,前端已经可以发请求到后端,后端也能拿到数据

接下来后端的逻辑应该是拿到这个账号密码去数据库核对是否存在,这里我省去这个步骤,假设一定有这个账号密码

routes/user.js

javascript 复制代码
// 和用户相关的接口
const router = require('koa-router')() // 直接调用

// 定义一个post请求的接口
router.post('/login', (ctx) => {
    let user = ctx.request.body // 拿到前端的参数
        if (1) { 
        ctx.body = {
            code: 0,
            data: `你好${user.username}`
        }
    } else { // 数据库中没有这个账号
         ctx.body = {
            code: 1,
            data: '账号后密码错误'
         }
    }
})

module.exports = router 

这样,就实现了后端将数据返回给前端,前端成功拿到数据

现在有个问题,就是我可以在用户界面不输入账号密码,直接修改url,跳去home主页去,照样是能过去的,首页一定是根据账号来进行展现的,因此账号不同首页数据不同,如何进行区分呢?后端返回一个code: 0一定是不够的,还需要返回一个token

token

参考:JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)

JWT(JSON Web Token)是目前最流行的跨域认证解决方案

跨域认证重点在认证二字

跨域认证流程

  1. 用户向服务器发送用户名和密码。

  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

    这是后端的session会话,非前端的sessionStorage,存在运存中

  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。

    这个session_id就是token令牌,大多数情况下浏览器的Cookie由后端控制

  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

cookie虽然是浏览器的内存空间,但是受后端掌控,后端将登录令牌保存在cookie

cookie有个特点,所有保存在cookie中的数据,都会在http请求时自动被携带在请求头中

再一段话解释下流程:用户发送账号密码给后端校验,后端通过校验,并在session中存入唯一数据,然后返回一个token给前端,将其写入到浏览器的cookie中,之后用户的每次请求都会把cookie带上,而cookie中又会有token,因此就是每次请求(比如这里的请求首页)都会把token带回给后端,后端会比较这个token和之前的自己生成的token是否一致,一致则同一账号

另外,再来想想一个情景:假设我有个账号已经登录在自己电脑里了,然后我在另外一个设备中也登录了这个账号,两个设备登录之后后端都会返回一个token回去,两个token会一样吗?必然是不一样的,因为token里面还会带上时间戳,在后端看来,我仅仅只维护最新的token,因此原来的设备你在个人首页走向个人详情页的时候,会带上一个token给到后端,后端发现,誒?我的token不是已经更新掉了吗,因此,这个token是非法的,后端会给你返回一个登录失效

这就是面试官常问的,账号在别处修改了账号密码原设备被顶掉了怎么办

当然,这其实是有两种情况的,一个是你在别处修改密码后,原设备没有发任何请求,你也被告知下线,这是借助websocket来实现的实时通信;还有一种是原设备可以留在原地,不受影响,但是一旦你点击刷新或者去到别处页面发请求,后端才会通知你登录失效

当然token也可以设置过期,过期同样登录失效

一个注意点:登录注册接口是不携带token的,因为token就是需要你先返回一个账号密码再去生成;

还有就是,将token存入cookies中是后端常干的行为,但是后端也可以偷懒,不给你存入,后端就相当于直接将token当成字符串直接返回给前端,ctx.body中多个token。既然如此,那么这个工作只能让前端来干了,前端自己将token存入cookie中去。当然浏览器的三大存储Local storageSession storageCookies,浏览器都可以进行操作,所以前端做这个工作,你爱存哪里存哪里,有时候前端可能做的是后台管理系统,登录需要就在当前会话中生效,退出页面就会失效,因此会将token存入Session storage。若是存入cookie,你可以设置过期时间,没有过期就不会失效

接下来的token实现我就前端来存入LocalStorage

另外,token如果长成这个样子会怎么办

这样不就是明文传输吗,被别人看到了怎么办,非常不安全!

假设后端真这样把token明文传给前端,小明在网吧准备给账号氪金,但是先去上了趟洗手间,这个过程中法外狂徒张三过来点击F12进入调试页面,将你的token中的账号字段改成张三自己的账号,最后小明氪金氪给了张三账号......

另外,token中是不会包含账号的密码的

但是后端生成的token确确实实是这样的对象,为保证安全,后端还会将其进行加密,最终长成如下这样

组成

标准的token包含三部分:

  • Header(头部)

    存放账号信息

  • Payload(负载)

    存放账号的描述

  • Signature(签名)

    签名就是标记的意思

这里特意强调标准token有着三部分,因此后端你也可以就是将token仅仅包含账号信息,这样很多需求就实现不了了

token实现

紧接着上面的情景,我现在就是让后端生成一个token,让前端自己存储,第一步,后端生成token,并进行加密并返回给前端,然后前端存入LocalStorage中,并跳转页面发请求

大概模拟下,就是如下这样

javascript 复制代码
const router = require('koa-router')() // 直接调用

router.post('/login', (ctx) => {
    let user = ctx.request.body // 拿到前端的参数
        if (1) { 
        ctx.body = {
            code: 0,
            data: `你好${user.username}`
        }
    } else { // 数据库中没有这个账号
         ctx.body = {
            code: 1,
            data: '账号后密码错误',
            token: 'xxxxxxx'  // 大概这样子
         }
    }
})

module.exports = router 

生成token

参考:jsonwebtoken - npm (npmjs.com)

国外已经有人造好了轮子,我们不需要自己写算法实现

后端安装好npm i jsonwebtoken

我把生成token的函数封装到server/utils/jwt.js

server/utils/jwt.js

写个sign函数用于生成token,既然token的生成依据账号,那就需要传入前端返回的账号对象,另外还需要个加密方式,这个参数称之为加盐,我就写个666

java 复制代码
// 封装一个可用于创建token的函数
const jwt = require('jsonwebtoken') 

function sign (option) { // 生成token
    return jwt.sign(option, '666', { // 第二个参数为加盐,666放到了账号中去
        expiresIn: 60 // token的有效时长 单位s
    }) 
}

module.exports = {
    sign
}

好了,现在拿到server/routes/user.js中调用这个函数生成token

javascript 复制代码
// 和用户相关的接口
const router = require('koa-router')() // 直接调用
const jwt = require('../utils/jwt.js')

// 定义一个post请求的接口
router.post('/login', (ctx) => {
    let user = ctx.request.body 
    if (1) { // 模拟验证成功
        // 创建一个token
        let jwtToken = jwt.sign({id: '1', username: user.username, admin: true}) // admin: true意思是管理员的token,权限
        console.log(jwtToken); // 已经加密好了
        ctx.body = {
            code: 0,
            data: `你好${user.username}`,
            token: jwtToken
        }
    } else { // 数据库中没有这个账号
         ctx.body = {
            code: 1,
            data: '账号后密码错误'
         }
    }
})

module.exports = router 

打印下,确实有,并且还已经加密好了,所以肯定给到了前端

前端也确确实实接收到了

好了,现在前端自己存到LocalStorage中去

前端存储

紧接着前端存储下来,然后跳转到首页,跳转页面用router.push(),记得引入路由

client/src/views/Login.vue

xml 复制代码
<script setup>
import axios from 'axios'
import { ref } from 'vue';
import { useRouter } from 'vue-router'

const router = useRouter()

const username = ref('')
const password = ref('') 

const onSubmit = (values) => {
    console.log(values)
    // // 发请求 发请求 封装axios 或者ajax的fetch,vue官方推荐你用axios,很好用的库
    axios.post('http://localhost:3000/login', values) 
    .then(res => {
        console.log(res.data.token); 
        localStorage.setItem('token', res.data.token)
        router.push('/home') // 来到首页需要发接口请求,然后把token给到后端
    })
}
</script>

好了,现在来到了首页,我们的token工作还远远没有完成,你还需要来到首页后发接口请求,并将token带入给后端

client/src/views/Home.vue

xml 复制代码
<script setup>
import { onMounted } from 'vue';

onMounted(() => {
    axios.get('/home') 
    .then((res) => {
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })
})
</script>

如果这样写,直接用axios发请求,那就和token没有任何关系,并且你甚至可以在登录页直接输入urlhome

因此我们需要在axios发请求的时候把token带给后端,我们不建议将token以?拼接传入后端,这样的话每个接口请求都会长这样,很丑~

因此,我们自己封装axios

封装axios

封装的目的就是保证每个请求可以把token带给后端,另外还需要做一个响应拦截,请求失败了或者token过期了也应该交给axios

不过是前端发请求,还是后端返回的响应都会经过axios,因此需要统一进行判断,如果code不是0那就是逻辑性错误,比如密码错了,如果这样就返回一个Promise出来,方便捕获错误,易于调试,这样刚才首页发接口请求就可以then后面写catch

逻辑性错误就是后端没有崩掉,是业务逻辑出错,后端崩掉是程序性错误

client/src/api/index.js

javascript 复制代码
import axios from 'axios'
import router from '../router'

axios.defaults.baseURL = "http://localhost:3000"

// 请求拦截 
axios.interceptors.request.use(config => {
    let token = localStorage.getItem('token')
    if (token) {
        config.headers.Authorization = token
    }
    return config // 把请求拦截下来,并往请求头中加入token,然后return 
})

// 做一个响应拦截,比如登录失败需要提示登录失败 发请求和接受都需要经过axios的手
axios.interceptors.response.use(
    (res) => {
        if (res.data.code && res.data.code !== 0) { // 逻辑性错误,比如密码敲错了,并不是程序性错误
            return Promise.reject(res.data.error) // 这么做的意义是,让axios好去调试,可以捕获错误
        }
        if (res.data.status >= 400 && res.data.status < 500) { // 程序性错误
            // 状态码在[400, 500) 就认为用户没有权限,就强行把你重定向到登录页面
            router.push('/login')
            return Promise.reject(res.data)
        }
        return res  // 响应的内容没有问题
    }
)

export function post(url, body) { 
    return axios.post(url, body)
}
  • 请求拦截就是把token拿到,如果存在token,存入到header中的Authorization字段中,这个字段是我们自己命名的
  • 如果状态码在[400, 500)之间就说明用户没有权限,比如没有登录就来请求数据,那就把他强行送到登录页

然后登录和首页的接口请求我再封装下,我把post刚刚被抛出的请求引入到下面这里

client/src/api/user.js

javascript 复制代码
import { post } from './index.js'

export function login(body) {
    return post('/login', body).then(res => {
        return res.data
    })
}

这样我的Login页面,就不需要原来axios请求了,直接把login函数拿来调用下就可以了,优雅!

Login.vue

xml 复制代码
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router'
import { login } from '../api/user.js'

const router = useRouter()

const username = ref('')
const password = ref('') 

const onSubmit = (values) => {
    login(values).then(res => {
        console.log(res);
        localStorage.setItem('token', res.token)
        router.push('/home') // 来到首页需要发接口请求,然后把token给到后端
    })
}
</script>

同样,home页的请求我也封装好

client/src/api/user.js

javascript 复制代码
import { post } from './index.js'

export function login(body) {
    return post('/login', body).then(res => {
        return res.data
    })
}

export function home() {
    return post('/home').then(res => {
        return res.data
    })
}

Home.vue

xml 复制代码
<script setup>
import { home } from '../api/user.js'
import { onMounted } from 'vue';

onMounted(() => {
    // axios.get('/home') // 不建议把token以?接在url后面,这样用了,所有页面都这样用很难看,所以封装下,复用
    home()
    .then((res) => {
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })
})
</script>

目前,后端还没有home接口,因此会报一个404的错误,并且这个请求头中会有authorization字段存放token,只要是404那么就会把你送回登录页

好了,现在去后端写首页的接口,以及token的校验

token校验

先把后端的home接口写好

server/routes/user.js

javascript 复制代码
router.post('/home', (ctx) => {
    ctx.body = {
        code: 0,
        data: '这是首页的数据'
    }
})

这样写没有进行校验,那么你前端就可以从登录页不登录修改url去到home

这样就是鉴权失败!

鉴权不仅仅是校验是否存在token还要去校验这个token是否是我当初加密了的token,当初我的那个token是加了个666的,要是你仅仅是看是否存在token,那么别人可以直接去浏览器应用页面新增乱写的token

这个分析方法同样我把它封装到jwt.js中,verify验证方法是jwt自带的,并且需要带上当初生成token的加盐参数,也就是666

如果前端传过来的token有问题,那么就向前端返回一个401的状态码,401就是无权,用户名,密码,令牌错误

校验成功就next放行,如果没有前端没有传过来token,那就同样向前端输出401

javascript 复制代码
// 封装一个可用于创建token的函数
const jwt = require('jsonwebtoken') // npm i jsonwebtoken

function sign (option) { // 生成token
    return jwt.sign(option, '666', { // 第二个参数为加盐,666放到了账号中去
        expiresIn: 60 // token的有效时长 单位s
    }) 
}

const verify = () => (ctx, next) => { // 校验token是否有效
    let jwtToken = ctx.req.headers.authorization // 前端传过来的authorization需要写成小写
    if (jwtToken) {
        jwt.verify(jwtToken, '666', (err, decoded) => {
            if (err) { // 前端传过来的token有问题
                ctx.body = {
                    status: 401, // 没有权限
                    message: 'token失效'
                }
            } else {
                // 校验成功
                next() // 放行
            }   
        })      
    } else {
        ctx.body = {
            status: 401, 
            message: '请提供token'
        }
    }
}

module.exports = {
    sign,
    verify
}

这个方法我拿到routes/user.js中调用

javascript 复制代码
router.post('/home', jwt.verify(), (ctx) => { // 请求这个地址时,校验失败就不走回调
    ctx.body = {
        code: 0,
        data: '这是首页的数据'
    }
})

现在检验下

我清空token数据,然后刷新首页,没有拿到首页数据,这样做同样也是直接输入url到首页的效果,并且最终被送回到登录页

我再把token清空掉,自己加一个token然后刷新首页,token失效,没有拿到首页数据,并且最终被送回到登录页

现在摆在用户面前的是,只有老老实实登录,才能拿到首页数据

至此,整个token的流程都已经实现完毕,前端登录后,将账号密码返回给后端,后端生成,加密token,并返回给前端,前端存入LocalStorage并通过axios在每一次请求拦截中将token存入请求头中的Authorization字段并返回给后端,后端进行校验是否为当初加密了的tokentoken合法才返回数据,否则返回401状态码告诉前端token失效,实现了登录鉴权~

说完这段话,留心面试官可能会问你浏览器三种存储的区别,以及各个状态码的含义

最后

实际开发中,需要根据场景需求,如果项目需要你是一个退出浏览器就登录失效,那么后端肯定是不会给你把token存入Cookies中去,这就需要我们前端自己存入到Session Storage中去

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端