全栈项目?那你说说你的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,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

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

相关推荐
gqkmiss21 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃27 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰31 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye37 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm39 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊1 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java1 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript