后端 API 处理流程
搭建 https 服务
首先需要将 https 证书拷贝到 Node 项目中,然后添加如下代码:
const fs = require('fs')
const https = require('https')
const privateKey = fs.readFileSync('./https/book.llmysnow.top.key', 'utf8')
const pem = fs.readFileSync('./https/book.llmysnow.top.pem', 'utf8')
const credentials = {
key: privateKey,
cert: pem,
}
const httpsServer = https.createServer(credentials, app)
httpsServer.listen(18082, () => {
console.log('running on https://127.0.0.1:%s', 18082)
})
创建 /user/login API
在 router/user.js 中添加如下代码:
router.post('/login', (req, res) => {
res.json({
code: 0,
msg: '登录成功',
})
})
这里我使用 Postman 测的接口,也可以使用
这里我们通过 req.body 获取 POST 请求参数无果,我们需要通过 body-parser 中间件来解决这个问题,新版本 express 内置了 POST 参数解析
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use('/', router)
Nodejs进阶:Express常用中间件body-parser实现解析
body-parser 主要实现如下:
- 处理不同类型的请求体,比如
text 、json 、urlencoded 等,对应的报文主体的格式不同 - 处理不同的编码,比如
utf8 、gbk 等 - 处理不同的压缩类型:比如
gzip 、deflare 等 - 其他边界、异常的处理
返回前端使用登录按钮请求登录接口,发现控制台报错:
Access to XMLHttpRequest at 'http://127.0.0.1:3003/user/login' from origin 'http://localhost:9527' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
因为前端部署在 http://localhost:9527 ,后端部署在 http://localhost:3003 ,所以导致了跨域错误,我们需要在 Node 中添加跨域中间件 cors
npm i cors
再次请求即可成功,这里我们在 Network 中会发现发起了两次 http 请求,这是因为触发跨域,所以会首先进行 OPTIONS 请求,判断服务端是否允许跨域请求,如果允许才能实际进行请求
app.use(cors())
什么时候会发送options请求
响应结果封装
在 /user/login 我们看到的返回值是:
res.json({
code: 0,
msg: '登录成功'
})
之后我们还要定义错误返回值,但如果每个接口都编写以上代码就显得非常冗余,而且不易维护。为了解决这个问题,我们创建一个 Result 类来解决这个问题
const { CODE_ERROR, CODE_SUCCESS } = require('../utils/constant')
class Result {
constructor(data, msg = '操作成功', options) {
this.data = null
if (arguments.length === 0) {
this.msg = '操作成功'
} else if (arguments.length === 1) {
this.msg = data
} else {
this.data = data
this.msg = msg
if (options) {
this.options = options
}
}
}
createResult() {
if (!this.code) {
this.code = CODE_SUCCESS
}
let base = {
code: this.code,
msg: this.msg,
}
if (this.data) {
base.data = this.data
}
if (this.options) {
base = { ...base, ...this.options }
}
console.log(base)
return base
}
json(res) {
res.json(this.createResult())
}
success(res) {
this.code = CODE_SUCCESS
this.json(res)
}
fail(res) {
this.code = CODE_ERROR
this.json(res)
}
}
module.exports = Result
我们还需要创建 /utils/constant.js :
module.exports = {
CODE_ERROR: -1,
CODE_SUCCESS: 0,
DEBUG: false
}
Result 使用了 ES6 的 Class,使用方法如下:
new Result().success(res)
new Result('登录成功').success(res)
new Result({ token }, '登录成功').success(res)
new Result('用户名或密码不存在').fail(res)
有了 Result 类后,我们可以将登陆 API 改为:
router.post('/login', (req, res) => {
const { username, password } = req.body
if (username === 'admin' && password === '123456') {
new Result('登录成功').success(res)
} else {
new Result('登录失败').fail(res)
}
})
登陆用户数据库查询
响应过程封装完毕后,我们需要在数据库中查询用户信息来验证用户名和密码是否准确
安装 mysql 库:
npm i mysql
创建 db 目录,创建两个文件 index.js 和 config.js ,在 config.js 中添加如下代码:
- 因为我电脑装了多个 mysql,所以我需要指定端口号,默认是 3306
module.exports = {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'book',
port: 3308
}
连接数据库:
const mysql = require('mysql')
const config = require('./config')
function connect() {
return mysql.createConnection({
host: config.host,
user: config.user,
password: config.password,
database: config.database,
port: config.port,
multipleStatements: true,
})
}
查询时调用 connection 对象的 query 方法
- 注意
conn 对象使用完毕后需要调用 end 进行关闭,否则会导致内存泄露
function querySql(sql) {
const conn = connect()
DEBUG && console.log(sql)
return new Promise((resolve, reject) => {
try {
conn.query(sql, (err, results) => {
if (err) {
DEBUG && console.log('查询失败,原因:' + JSON.stringify(err))
reject(err)
} else {
DEBUG && console.log('查询成功', JSON.stringify(results))
resolve(results)
}
})
} catch (err) {
reject(err)
} finally {
conn.end()
}
})
}
可以把 sql 语句直接写在 router/user.js ,更好的做法是封装一层 sevice,用来协调业务逻辑和数据库查询,创建 service/user.js
-
密码采用了 MD5 + SALT 加密 当然加密方法还有很多,可以看我这篇文章:前后端 JS 加密常用方法(非对称加密、对称加密) npm i crypto
创建在 utils/index.js ,添加如下内容: const crypto = require('crypto')
function md5(s) {
return crypto.createHash('md5').update(String(s)).digest('hex')
}
module.exports = { md5 }
utils/constant.js 中添加如下内容:
module.exports = {
PWD_SALT: 'admin_imooc_node'
}
再次输入正确的用户名密码 admin admin ,即可查询成功
const { PWD_SALT } = require('../utils/constant')
router.post('/login', (req, res) => {
let { username, password } = req.body
password = md5(`${password}${PWD_SALT}`)
login(username, password).then(user => {
console.log(user)
if (!user || user.length === 0) {
new Result('登录失败').fail(res)
} else {
new Result('登录成功').success(res)
}
})
})
express-validator 表单验证
- 它是一个功能强大的表单验证器,它是
validator.js 的中间件
express-validator
使用 express-validator 可以简化 POST 请求的参数验证,使用方法如下:
npm i express-validator
router.post(
'/login',
[
body('username').isString().withMessage('用户名必须为字符'),
body('password').isString().withMessage('密码必须为字符'),
],
(req, res, next) => {
const err = validationResult(req)
if (!err.isEmpty()) {
const [{ msg }] = err.errors
next(boom.badRequest(msg))
} else {
let { username, password } = req.body
password = md5(`${password}${PWD_SALT}`)
login(username, password).then(user => {
console.log(user)
if (!user || user.length === 0) {
new Result('登录失败').fail(res)
} else {
new Result('登录成功').success(res)
}
})
}
}
)
express-validator 使用技巧:
- 在
router.post 方法的第二个参数里面,使用 body 方法判断参数类型,并指定出错的提示信息 - 使用
const err = validationResult(req) 获取错误信息,err.errors 是一个数组,包含所有错误信息,如果 err.errors 为空表示校验成功,没有参数错误 - 如果发现错误我们可以使用
next(boom.badRequest(msg)) 抛出异常,交给我们自定义的异常处理方法进行处理
JWT
Token
Token 本质是字符串,用于请求时附带请求头中,校验请求是否合法及判断用户身份
Token、Session、Cookie 的区别
Session 保存在服务端,用于客户端与服务端连接时,临时保存用户信息,当用户释放连接后,Session 将被释放Cookie 保存在客户端,当客户端发起请求时,Cookie 会附带在 http header 中,提供给服务端辨识用户身份Token 请求时提供用于校验用户是否具备访问接口的权限
其他方面的区别,可以参考我这篇文章: HTTP网络层性能优化
Token 用途
JWT
JSON Web Token(JWT )是非常流行的跨域身份验证解决方案。jwt 官网
上面 Encoded 那段 JWT 字符串,他被解析成以下三部分:
-
HEADER : ALGORITHM & TOKEN TYPE header 是描述 JWT 元数据的 JSON 对象:
- alg:表示加密算法,
HS256 是 HMAC SHA256 的缩写 - type:
Token 类型 {
"alg": "HS256",
"typ": "JWT"
}
-
PAYLOAD : DATA payload 是 JWT 的主体内容部分,也是一个 JSON 字符串,包含需要传递的数据,注意 payload 部分不要存储隐私数据,防止信息泄露 {
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
-
VERIFY SIGNATURE JWT 签名部分是对上面两部分数据加密后生成的字符串,通过 header 指定的算法生成加密字符串,以确保数据不会被篡改 生成签名时需要使用秘钥,秘钥只保存在服务端,不能向用户公开,它是一个字符串,我们可以自由设定 生成签名时需要根据 header 中指定的签名算法,并根据下方的公式,即将 header 和 payload 的数据通过 BASE64 加密后采用 . 进行连接,然后通过秘钥进行 SHA256 加密,由于加入了秘钥,所以生成的字符串将无法被破译和篡改,只有在服务端才能还原 HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
生成 JWT Token
安装
npm i jsonwebtoken
使用
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')
login(username, password).then(user => {
if (!user || user.length === 0) {
new Result('登录失败').fail(res)
} else {
const token = jwt.sign({ username }, PRIVATE_KEY, { expiresIn: JWT_EXPIRED })
new Result({ token }, '登录成功').success(res)
}
})
utils/constant.js ,这里需要定义 jwt 的私钥和过期时间,过期时间不宜过短,也不宜过长,根据业务场景来把控
module.exports = {
PRIVATE_KEY: 'admin_imooc_node_private_key',
JWT_EXPIRED: 60 * 60,
}
前端返回结果如下:
{
code: 0,
msg: '登录成功',
data: {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjM3MDMwNjcyLCJleHAiOjE2MzcwMzQyNzJ9.Rc6KMMjs025lT4aVMMfk2bUcJnlSwVGBWaDQm-hVTY8',
},
}
我们可以将该 Token 在 jwt 官网 进行验证,可以得到如下结果:
- 鼠标划在
iat 和 exp 上即可看到生成时间和过期时间
{
"username": "admin",
"iat": 1637030672,
"exp": 1637034272
}
可以看到 username 被正确解析,说明 Token 生成成功
JWT 认证
主要功能:检查所有路由是否有过期的 Token ,如果没有过期则验证为通过
安装
npm i express-jwt
创建 router/jwt.js
const expressJwt = require('express-jwt')
const { PRIVATE_KEY } = require('../utils/constant')
const jwtAuth = expressJwt({
secret: PRIVATE_KEY,
credentialsRequired: true,
}).unless({
path: ['/', '/user/login'],
})
module.exports = jwtAuth
启动 Node 时会发现报错:
通过 express-jwt 中间件进行验证,通过 unless 设置白名单,并在所有请求前使用
const expressJwt = require('express-jwt')
const { PRIVATE_KEY } = require('../utils/constant')
const jwtAuth = expressJwt({
secret: PRIVATE_KEY,
algorithms: ['HS256'],
credentialsRequired: true,
}).unless({
path: ['/', '/user/login'],
})
module.exports = jwtAuth
在 router/index.js 中使用中间件
const jwtAuth = require('./jwt')
router.use(jwtAuth)
重新启动,点击登录后端返回如下内容
为了使 Token 过期报错区别于其他错误,在 utils/constant.js
module.exports = {
CODE_TOKEN_EXPIRED: -2
}
修改自定义异常
router.use((err, req, res, next) => {
if (err.name && err.name === 'UnauthorizedError') {
const { status = 401, message } = err
new Result(null, 'Token验证失败', {
error: status,
errorMsg: message,
}).jwtError(res.status(status))
} else {
const msg = (err && err.message) || '系统错误'
const statusCode = (err.output && err.output.statusCode) || 500
const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
new Result(null, msg, {
error: statusCode,
errorMsg,
}).fail(res.status(statusCode))
}
})
重新启动,点击登录后端返回如下内容
前端处理 JWT Token
前端登录请求改造,修改 src/utils/request.js 中的响应拦截器
- 对
error 对象进行解构,取其中的 response.data.msg ,并对其进行提示
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 0) {
const errMsg = res.msg || '请求失败'
Message({
message: errMsg,
type: 'error',
duration: 5 * 1000
})
if (res.code === 2) {
MessageBox.confirm('Token已失效,请重新登录', '确认退出登录', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(errMsg))
} else {
return res
}
},
error => {
console.log({ error })
const { msg } = error.response.data
Message({
message: msg || '请求失败',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
后端添加路由的 jwt 认证后,再次请求 /user/info 将抛出 401 错误,这是由于前端未传递合理的 Token 导致的,需要对其请求拦截器中的 headers 进行修改
- 修改
headers 中为 Authorization - 在
getToken() 之前加一个 Bearer (后面有一个空格)
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['Authorization'] = `Bearer ${getToken()}`
}
return config
},
error => {
return Promise.reject(error)
}
)
前端去掉 /user/info 请求时传入的 Token ,因为我们已经从 Token 中传入,修改 src/api/user.js
export function getInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
用户查询 /user/info
查询用户只有一个人,querySql 查询的是多个人,返回的是一个数组,需要增加一个 queryOne 方法
function queryOne(sql) {
return new Promise((resolve, reject) => {
querySql(sql)
.then(results => {
if (results && results.length > 0) {
resolve(results[0])
} else {
resolve(null)
}
})
.catch(err => {
reject(err)
})
})
}
function findUser(username) {
const sql = `select * from admin_user where username='${username}'`
return queryOne(sql)
}
请求 /user/info 接口会得到如下内容:
- 这里
password 为敏感字段,需要对其进行删除
修改后代码如下(当然也可以在获取 user 以后进行处理,不过不推荐这么做):
function findUser(username) {
return queryOne(`select id, username, nickname, role, avatar from admin_user
where username='${username}'`)
}
前端在 HTTP header 中传入了 Token ,通过 Token 获取 username 就需要对 jwt token 进行解析,在 utils/index.js 中添加如下内容
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('./constant')
function decoded(req) {
let token = req.get('Authorization')
if (token.indexOf('Bearer') >= 0) {
token = token.replace('Bearer ', '')
}
return jwt.verify(token, PRIVATE_KEY)
}
修改 router/user.js
router.get('/info', (req, res) => {
const decode = decoded(req)
if (decode && decode.username) {
findUser(decode.username).then(user => {
if (user) {
user.roles = [user.role]
new Result(user, '用户信息查询成功').success(res)
} else {
new Result(user, '用户信息查询失败').fail(res)
}
})
}else{
new Result('用户信息查询失败').fail(res)
}
})
logout 方法
- 修改
src/store/modules/user.js
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
try {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resetRouter()
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
} catch (e) {
reject(e)
}
})
}
RefreshToken
场景:需要授权给第三方 APP
通常我们需要再增加一个 RefreshToken 的 API,该 API 的用途是根据现有的 Token 获取用户名,然后生成一个新的 Token ,这样做的目的是为了防止 Token 失效后退出登录,所以 APP 一般都会在打开时刷新一次 Token
|