密钥认证步骤:
- 用户首先在 React 中通过登录表单实现登录
- 这会使得 React 代码将用户名和密码通过/api/login 作为一个 HTTP POST 请求发送给服务器。
- 如果用户名和密码是正确的,服务器会生成一个 token,用来标识登录的用户。
- 这个 Token 是数字化签名的,也就是它不可能被伪造(使用加密手段)。
- 后台通过状态码返回一个 response, 表示操作成功,同时返回的还有这个 token。
- 浏览器将这个 token 保存到 React 应用的状态中
- 当用户请求创建一个新的 Note(或者做一些需要认证的操作), React 会通过 requset 发送这个 token 给 server
- server 便可以通过这个 token 来验证用户
安装jsonwebtoken库,用于生成json web token
npm install jsonwebtoken
登录功能的代码放到 controllers/login.js 中
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const loginRouter = require('express').Router()
const User = require('../models/user')
loginRouter.post('/', async (request, response) => {
const body = request.body
const user = await User.findOne({ username: body.username })
const passwordCorrect = user === null
? false
: await bcrypt.compare(body.password, user.passwordHash)
if (!(user && passwordCorrect)) {
return response.status(401).json({
error: 'invalid username or password'
})
}
const userForToken = {
username: user.username,
id: user._id,
}
const token = jwt.sign(userForToken, process.env.SECRET)
response
.status(200)
.send({ token, username: user.username, name: user.name })
})
module.exports = loginRouter
将login路由添加到app.js中
const loginRouter = require('./controllers/login')
//...
app.use('/api/login', loginRouter)
别忘了在.env中添加环境变量SECRET
测试login, 成功,生成了一个token
Limiting creating new notes to logged in users
更改创建note的逻辑,只有合法token的request才能通过
有几种方法可以将令牌从浏览器发送到服务器中。我们将使用Authorization 头信息。头信息还包含了使用哪一种authentication scheme 。如果服务器提供多种认证方式,那么认证 Scheme 就十分必要。这种 Scheme 用来告诉服务器应当如何解析发来的认证信息。
Bearer schema 正是我们需要的。
Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW
修改创建note的代码
const jwt = require('jsonwebtoken')
// ...
const getTokenFrom = request => {
const authorization = request.get('authorization')
if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
return authorization.substring(7)
}
return null
}
notesRouter.post('/', async (request, response) => {
const body = request.body
const token = getTokenFrom(request)
const decodedToken = jwt.verify(token, process.env.SECRET)
if (!token || !decodedToken.id) {
return response.status(401).json({ error: 'token missing or invalid' })
}
const user = await User.findById(decodedToken.id)
const note = new Note({
content: body.content,
important: body.important === undefined ? false : body.important,
date: new Date(),
user: user._id
})
const savedNote = await note.save()
user.notes = user.notes.concat(savedNote._id)
await user.save()
response.json(savedNote)
})
测试,在request中加入authorization
注意: JSON的最后一行不能有逗号
Error handling
token 可能是错误的(本例)、或者是伪造的或过期的。让我们来展开 errorHandler 中间件,来考虑不同的解码错误。
const unknownEndpoint = (request, response) => {
response.status(404).send({ error: 'unknown endpoint' })
}
const errorHandler = (error, request, response, next) => {
if (error.name === 'CastError') {
return response.status(400).send({
error: 'malformatted id'
})
} else if (error.name === 'ValidationError') {
return response.status(400).json({
error: error.message
})
} else if (error.name === 'JsonWebTokenError') {
return response.status(401).json({
error: 'invalid token'
})
}
logger.error(error.message)
next(error)
}
Problems of Token-based authentication
限制token 的有效时间
login.js中修改, 添加参数
const token = jwt.sign(
userForToken,
process.env.SECRET,
{ expireIn: 60 * 60 }
)
一旦token 过期, 客户端程序需要重新获取一个新的token。通常通过强制用户重新登录app 的方式实现。
一旦token过期, 错误处理中间件应当扩展来给出一个合适的错误提示
const errorHandler = (error, request, response, next) => {
logger.error(error.message)
if (error.name === 'CastError') {
-- snip --
} else if (error.name === 'TokenExpiredError') {
return response.status(401).json({
error: 'token expired'
})
}
next(error)
}