我们很多应用都是有用户权限的,如果校验通过,接口就返回数据;否则就提示401未授权。这里面逻辑是怎样的,这篇笔记介绍如何使用jsonwebtoken 进行鉴权
JWT 的流程本质就是一句话:
服务端生成 Token 给客户端,客户端之后每次请求带上 Token,服务端验证 Token 来决定是否放行。
- 客户端发送用户信息
- 服务端校验用户信息,生成token, 把token返回客户端
- 客户端请求带上token, config.headers.Authorization = Bearer ${token}
- 服务端验证token, 如果通过就放行 jwt.verify(token, secret)
校验的业务流程
- 登录阶段
- 客户端(浏览器/app)把用户名和密码发给服务端
- 服务端验证通过后,生成一个JWT Token, 把用户信息(比如userId)编码进去,然后签名
- 服务端把这个Token返回给客户端
- 后续请求阶段
- 客户端把这个Token保存起来(通常是放在localStorage或cookie里)
- 每次发请求时,把这个Token放在请求头里
js
Authorization: Bearer <token>
- 服务端验证阶段
- 服务端拿到这个Token, 用之前的密匙(secret)验证签名
- 如果验证成功,就可以找到这个请求是谁发的(从payload中解析出用户信息),并放行
- 如果Token无效或过期,就拒绝请求(通常返回401未授权)
代码具体实现
我们后端使用的是express, 安装jsonwebtoken包
bash
npm i jsonwebtoken
前端客户端发送用户信息 (下面代码是用户注册的时候)
js
const handleSignUp = async e => {
e.preventDefault()
if (!name) {
setError('请输入你的名字')
return
}
if (!validateEmail(email)) {
setError('请输入一个有效的邮件地址')
return
}
if (!password) {
setError('请输入密码')
return
}
setError('')
// Signup API Call
try {
const res = await axiosInstance.post('/create-account', {
fullName: name,
email,
password,
})
// Handle successful login response
if (res.data && res.data.accessToken) {
localStorage.setItem('token', res.data.accessToken)
navigate('/dashboard')
}
} catch (error) {
setError(error?.response?.data?.message || '未知错误.')
} finally {
setLoading(false)
}
}
在首页,处理用户请求,拿到用户信息,校验通过就生成签名
js
const jwt = require('jsonwebtoken')
const express = require('express')
const cors = require('cors')
const bcrypt = require('bcrypt')
const User = require('./models/user.model')
const app = express()
app.use(express.json())
app.use(cors({ origin: '*' }))
app.post('/create-account', async (req, res) => {
const { fullName, email, password } = req.body
if (!fullName || !email || !password) {
return res.status(400).json({
error: true,
message: '所有字段必填',
})
}
const isUser = await User.findOne({ email })
if (isUser) {
return res.status(400).json({
error: true,
message: '用户已经存在',
})
}
const hashedPassword = await bcrypt.hash(password, 10)
const user = new User({
fullName,
email,
password: hashedPassword,
})
await user.save()
const accessToken = jwt.sign(
{ userId: user._id },
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: '72h',
}
)
return res.status(201).json({
error: false,
user: { fullName: user.fullName, email: user.email },
accessToken,
message: 'Registration successful',
})
})
注册通过后,服务端把生成的token返回给客户端,客户端获取签名,存在localStorage里
js
if (res.data && res.data.accessToken) {
localStorage.setItem('token', res.data.accessToken)
navigate('/dashboard')
}
然后在接下来需要权限的的请求里,都把这个token带上, axiosInstance.ts
js
import axios from 'axios'
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
}
})
axiosInstance.interceptors.request.use(
config => {
const accessToken = localStorage.getItem('token')
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
},
error => {
return Promise.reject(error)
}
)
export default axiosInstance
接着我们在服务端处理用户发过来的请求
php
const { authenticateToken } = require('./utilities')
app.get('/get-all-stories', authenticateToken, async (req, res) => {
const { userId } = req.user
try {
const travelStories = await TravelStory.find({ userId: userId }).sort({
isFavourite: -1,
})
res.status(200).json({ stories: travelStories })
} catch (error) {
res.status(500).json({ error: true, message: error.message })
}
})
注意这里的authenticateToken, 我们先是获取req.headers['authorization'], 这里a
是小写,大小写没关系,(在 HTTP 协议中头部字段名不区分大小写,客户端用大写、服务端用小写是可行的),利用jwt.verify 进行验证,验证通过了就会继续执行请求
scss
const jwt = require('jsonwebtoken')
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
// No token, unauthorized
if (!token) return res.sendStatus(401)
// Verify token
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
// Token invalid, forbidden
if (err) return res.sendStatus(401)
req.user = user
next()
})
}
module.exports = {
authenticateToken,
}
one more thing
这段代码里的'application/json' 和 app.use(express.json())的作用分别是什么,有关联吗
js
// 前端接口拦截
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
}
})
// 服务端设置接口处理请求参数
app.use(express.json())
'Content-Type': 'application/json'
的作用是告诉服务器:我发送的数据是 JSON 格式
js
axiosInstance.post('/create-account', {
fullName: '孙悟空',
email: '[email protected]',
password: 'qitiandasheng',
})
配合 'Content-Type': 'application/json',axios 会把数据变成这样发送给服务器:
http
POST /login HTTP/1.1
Content-Type: application/json
{
"fullName": "孙悟空",
"email": "[email protected]",
"password": "qitiandasheng",
}
app.use(express.json())
就是告诉服务器: 如果客户端发过来的数据是 JSON,我能自动解析并放到 req.body 里。就是说前面发的孙悟空信息,可以直接拿到
perl
app.post('/login', (req, res) => {
console.log(req.body); // { "fullName": "孙悟空","email": "[email protected]", "password": "qitiandasheng"}
});
如果前端用的是 application/x-www-form-urlencoded
js
axios.post('/login', qs.stringify({ username: '悟空', password: 'wukong' }), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
这个时候,数据会以这种格式发送给服务端(key=value&key=value 的形式):
js
username=%E6%82%9F%E7%A9%BA&password=wukong
请求头 Content-Type | 前端格式 | 服务端解析方法 | req.body 是否能正常读取 |
---|---|---|---|
application/json | JSON.stringify(data) | express.json() | ✅ 是 |
application/x-www-form-urlencoded | qs.stringify(data) | express.urlencoded({ extended: true }) | ✅ 是 |
没有设置或格式不符 | 自动 fallback 或出错 | 无匹配中间件,Express 不会解析 | ❌ 否,req.body 是 undefined |
总结
前端的 Content-Type: application/json 是说"我发的是 JSON",后端的 express.json() 是说"我能识别 JSON"。它们必须一起使用,数据才能顺利沟通