从思考到动手只有一步之遥
前端登录成功,后端返回一个 token
,前端将该 token
保存在本地,在接下来的所有的接口请求中,前端都将带上此 token
用作身份识别…这个概念很多人都知道,但如果你仅仅只是知道概念,这篇文章希望你能认真读完它,带你一步一步用实践代码彻底理解 token
的来龙去脉(对新手有好,废话稍多,没办法作者一向婆妈心肠)
1. 什么是token?
用最直白的话来讲,token
是后端生成的,一串加密过的,可以用来表示用户身份的字符串。通常后端如何生成这样一个 token
呢?目前生成token的方式用的比较多的是JWT(JSON Web Token)
,你要是想问JWT是个什么玩意儿,就往下看。JWT在服务器认证之后,生成一个
{
"姓名": "蜗牛",
"角色": "帅哥",
"到期时间": "2022年3月8日0点0分"
}
复制代码
这样的JSON对象返回给前端,但是为了安全性考虑,直接返回这样的json对象显然是很危险的(用户可以篡改数据)所以实际上JWT生成出来的token,还会做加密处理,变成这样:
注意: token本身没有换行,这里换行的地方是为了展示效果而已,三种不同的颜色分别代表着
- Header(头部)
- Payload(负载)
- Signature(签名)
对这这三个名词没有概念的同学此刻已经打开了搜索引擎
2. 为什么要用token?
今天是女神节,你女朋友在她的淘宝购物车里面添加了满满一车的商品,她将购物车地址甩给你,你可以在你电脑上直接看到她的购物车清单吗,当然不能。你得登录她的淘宝账号才有权限看到购物车清单,这里就是典型的权限控制,如果淘宝的后台不能识别当前正在访问购物车页面的用户是你女朋友的话,清单数据是不会展示出来的,如果展示出来了,可想而知,你才是那个最慌的~~~ 所以token也称为令牌,专门用来做用户身份识别的
3. 前后端准备工作
我们先创建一个 jwt 的文件夹,再在其中创建一个前端项目和一个后端项目,懒如我就直接用个vue3创建个项目
当前目录结构:
借助vantUI我们先写一个前端登录页面
<!-- src/views/Login.vue -->
<template>
<van-form @submit="onSubmit" class="login-form">
<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>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const username = ref('');
const password = ref('');
const onSubmit = (values) => {
console.log('submit', values);
};
return {
username,
password,
onSubmit,
};
},
};
</script>
<style lang="less">
.login-form{
margin-top: 30px;
}
</style>
复制代码
配置该页面的路由 /login 后页面能正常展示
点击登录已经能获取到用户输入的账号和密码,那么我们就去发起登录接口请求吧,前端在发送请求之前先安装一个 axios ,再创建目录 src > api > index.js
// src > api > index.js
import axios from 'axios'
import router from '../router'
const BASE_URL = 'http://localhost:3000'
// 请求拦截
axios.interceptors.request.use((config) => {
let jwtToken = window.localStorage.getItem('jwtToken')
if (jwtToken) {
// 将token携带在请求头中
config.headers.Authorization = jwtToken
}
return config
})
// 响应拦截
axios.interceptors.response.use(res => {
// 拦截报错的情况
if (res.data.code !== 0) {
return Promise.reject(res.data.error)
}
return res
}, (error) => {
// status在400~500之间的我们认定为是登录过期
if (error.response.status >= 400 && error.response.status < 500) {
router.push('/login')
}
return Promise.reject(error.response.data.error)
})
export function post(url, body) {
return axios.post(BASE_URL+url, body)
}
复制代码
在api文件夹中创建user.js,再封装一个登录函数:
// src > api > user.js
import { post } from './index'
export function login(body) {
return post('/login', body).then(res => res.data)
}
复制代码
将封装好的函数引入到登录页使用:
<script>
import { ref } from 'vue';
import { login } from '../api/user' // 新增代码
export default {
setup() {
const username = ref('');
const password = ref('');
const onSubmit = (values) => {
console.log('submit', values);
// 新增代码
login(values).then(res => {
console.log(res);
})
};
return {
username,
password,
onSubmit,
};
},
};
</script>
复制代码
此时我们已经完成前端的准备工作,接下来只需要后端提供登录接口便可以。来到后端项目, 这里我用的 Koa 来写的后端,安装项目所需的依赖 npm i koa koa-router koa2-cors koa-bodyparser
来到文件夹 server > app.js
// server > app.js
const Koa = require('koa')
const cors = require('koa2-cors') // 处理跨域
const bodyParser = require('koa-bodyparser'); // 帮助koa解析post请求的参数
const app = new Koa()
app.use(bodyParser());
app.use(cors());
app.listen(3000, () => console.log('服务已在3000端口启动'))
复制代码
接下来使用路由完成接口定义,在server文件夹下创建routes文件夹,再在routes中创建user.js
const router = require('koa-router')()
router.post('/login', (ctx) => {
let user = ctx.request.body
console.log(user);
// 模拟数据库检验
if (1) {
ctx.body = {
code: 0,
data: user.username,
}
} else {
ctx.body = {
code: 1,
data: '用户名或密码错误'
}
}
})
router.post('/home', (ctx) => {
try {
ctx.body = {
code: 0,
data: "got home page"
}
} catch (error) {
ctx.body = {
code: 1,
data: error
}
}
})
module.exports = router
复制代码
再启用路由
// server > app.js
const Koa = require('koa')
const cors = require('koa2-cors') // 处理跨域
const bodyParser = require('koa-bodyparser'); // 帮助koa解析post请求的参数
+ const userRouter = require('./routes/user')
const app = new Koa()
app.use(bodyParser());
app.use(cors());
+ app.use(userRouter.routes(), userRouter.allowedMethods())
app.listen(3000, () => console.log('服务已在3000端口启动'))
复制代码
运行nodemon app.js你会看到项目已经正常启动
接下来我们到页面上输入任意的 账号 和 密码 点击登录按钮你会看到后端打印
成功获取到了前端传递过来的参数,那么接下来才是本文要讲述的核心了
当后端拿到前端登录请求传递的参数后,正常来说应该去数据库校验当前的 账号 和 密码 是否正确,如果正确的话,应该向前端返回登录成功
并且返回一个token
,那么后端如何生成这个token呢?前面我们提到了JWT
,我们正是用JWT
来生成token
4. 后端如何生成token
我们需要在后端项目中 安装jsonwebtoken
,执行 npm i jsonwebtoken
,在server下创建utils文件夹,再在utils里面创建一个 jwt.js 文件
// server > utils > jwt.js
const jwt = require('jsonwebtoken')
// sign用于生成token,666作为加密的私钥可以自行定义
function sign(option) {
return jwt.sign(option, '666', {
expiresIn: 60 // 当前设定过期时间在60秒之后
})
}
module.exports = {
sign
}
复制代码
回到路由文件user.js中使用jwt
// server > routes > user.js
const router = require('koa-router')()
+ const jwt = require('../utils/jwt')
router.post('/login', (ctx) => {
let user = ctx.request.body
console.log(user);
// 模拟数据库检验
if (1) {
+ let jwtToken = jwt.sign({id: '1023', username: ctx.request.body.username, admin: true})
+ console.log(jwtToken);
ctx.body = {
code: 0,
data: 'success',
token: jwtToken
}
} else {
ctx.body = {
code: 1,
data: '用户名或密码错误'
}
}
})
复制代码
利用用户id(这里是写死的假数据)和用户名,并且设定为管理员权限,来生成一个token,前端再次发登录请求你会看到
后端已经生成好了一份具有时效性的token,接下里的工作就是将这个token在登录接口中响应回去
// server > routes > user.js
// 省略部分代码
if (1) {
let jwtToken = jwt.sign({id: '1023', username: ctx.request.body.username, admin: true})
console.log(jwtToken);
ctx.body = {
code: 0,
data: user.username,
+ token: jwtToken
}
}
// 省略部分代码
复制代码
再次发起前端的登录请求,我们在浏览器的控制台可以看到
剩下的工作就好做了,前端获取到token,存在本地存储中
// login.vue
// 省略部分代码
<script>
import { ref } from 'vue';
import { login } from '../api/user'
// 新增代码
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter()
const username = ref('');
const password = ref('');
const onSubmit = (values) => {
login(values).then(res => {
// 新增代码
window.localStorage.setItem('jwtToken', res.token)
router.push('/home')
})
};
return {
username,
password,
onSubmit,
};
},
};
</script>
// 省略部分代码
复制代码
好了登录接口顺利完成了,前端也拿到了token保存在本地,接下来前端的每次请求都会在请求头里携带上token,到这里你是不是觉得就结束了,不不不,还有一个关键性的问题,如果用户没有登录而是直接手动跳转到 /home
路径下,后端还返回数据嘛,当然不返回,那么后端就需要校验当用户请求 home首页的数据时,是否已经登录或者登录是否失效,换句话说,后端要校验用户请求首页数据时请求头中的token是否合法
5. JWT的合法、权限校验
让我们简单的创建一个首页出来
// src > views > Home.vue
<template>
<div>Home</div>
</template>
<script>
import { onMounted } from 'vue'
import { home } from '../api/user'
export default {
setup() {
onMounted(() => {
home().then(res => {
console.log(res);
})
})
return {
}
}
}
</script>
<style>
</style>
复制代码
并配置好Home页面的路由,在api下的user.js中再封装Home页面要用的请求函数
// src > api > user.js
import { post } from './index'
export function login(body) {
return post('/login', body).then(res => res.data)
}
// 新增代码
export function home(body) {
return post('/home', body).then(res => res.data)
}
复制代码
再回来浏览器 /home
页面上时
后端接口前面已经写过了,最后这个问题我们需要回到后端代码中
// server > utils > jwt.js
const jwt = require('jsonwebtoken')
// sign用于生成token,666作为加密的私钥可以自行定义
function sign(option) {
return jwt.sign(option, '666', {
expiresIn: 60 // 当前设定过期时间在60秒之后
})
}
// 新增代码
// isAdmin参数用于权限控制
let verify = (isAdmin) => (ctx, next) => {
// 获取到前端传递多来的token
let jwtToken = ctx.req.headers.authorization
if (jwtToken) {
// 校验token的合法性
jwt.verify(jwtToken, '666', function(err, decoded) {
if (err) {
ctx.body = {
status: 401,
message: 'token失效'
}
} else {
if (isAdmin) {
let { admin } = decoded
if (admin) {
next()
} else {
ctx.body = {
status: 401,
message: '你不是管理员!权限不够!'
}
}
} else {
next()
}
}
});
} else {
ctx.body = {
status: 401,
message: '请提供token'
}
}
}
module.exports = {
sign,
verify
}
复制代码
这里将 jwt.verify
方法做了二次封装,抛出 verify
函数,将这个函数拿到路由接口中使用
// server > routes > user.js
// (省略部分代码)
// 修改代码,传入第二参数 jwt.verify()
router.post('/home', jwt.verify() , (ctx) => {
try {
ctx.body = {
code: 0,
data: "got home page"
}
} catch (error) {
ctx.body = {
code: 1,
data: error
}
}
})
复制代码
如此/home
接口被请求时,就会触发jwt的校验,我们来试试效果
当我们手动删除浏览器本地存储的 token
后,在/home
下刷新页面
现在你不登录后端是不会返回数据的,当我们重新登录,来到/home
页面,等待1分钟之后,再次刷新home页面时,你同样会看到
奶思!!!最后,这里可能有小伙伴会问,写死的token过期时间岂不是有问题,当然是的,所以正常的操作应该是,当后端在规定的时间没有被任何请求数据的时候,才让token过期的
结语
恭喜你对token的理解又更加深刻了,如果觉得有帮助欢迎点赞关注。源码地址在这里
文章参考: