- 原文地址:You don't need passport.js - Guide to node.js authentication
- 原文做者:Sam Quinn
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:HytonightYX
- 校对者:HZNU-Qiu,xionglong58
诸如 Google Firebase,AWS Cognito 以及 Auth0 这样的第三方认证服务愈来愈流行,相似于 passport.js 这样的一站式解决方案也成为了业界标准,可是一个广泛状况是,开发者们其实并不清楚完整的认证流程到底涉及那些部分。javascript
这一系列关于 node.js 认证的文章,旨在让你搞清楚一些概念,好比 JSON Web Token (JWT)、社交帐号登陆 (OAuth2)、用户模仿(一个管理员无需密码便能做为特定用户登陆)。前端
固然,文末也给你准备好了一个完整的 node.js 认证流程的代码库,放在GitHub上了,你能够做为你本身项目的基础来使用。java
在阅读以前,你须要先了解:node
在我写下这篇文章之时,我认为 Argon2 是目前最好的加密算法,请不要用 SHA256,SHA512 或者 MD5 这类简单的加密算法了。android
有关这点,有兴趣的话能够去看看这篇很是棒的文章 choosing a password hashing algorithm(如何选择密码哈希算法)。ios
新用户建立帐户时,必须对密码进行哈希处理并将其与电子邮件和其余详细信息(好比用户配置文件、时间戳等等)一块儿存储在数据库中。git
提示:你能够去以前的文章了解 node.js 的项目结构 Bulletproof node.js project architecture 🛡️github
import * as argon2 from 'argon2';
class AuthService {
public async SignUp(email, password, name): Promise<any> {
const passwordHashed = await argon2.hash(password);
const userRecord = await UserModel.create({
password: passwordHashed,
email,
name,
});
return {
// 绝对不要返回用户的密码!!!!
user: {
email: userRecord.email,
name: userRecord.name,
},
}
}
}
复制代码
数据库中,这名用户的记录看起来就是这样:web
当一名用户想要登陆时,会发生下面的事情:算法
客户端发送成对的公共标识(Public Identification)和私钥(Private key)
服务端根据发来的 email 去数据库查找用户记录。
若是找到了,服务端会将收到的密码进行哈希,而后和数据库中已经哈希过的密码进行比对。
若是这两个哈希值对上了,那么服务端就发一个 JSON Web Token (JWT)。
这个 JWT 就是一个临时 key,客户端每次发器请求都须要带上这个 Token
import * as argon2 from 'argon2';
class AuthService {
public async Login(email, password): Promise<any> {
const userRecord = await UserModel.findOne({ email });
if (!userRecord) {
throw new Error('User not found')
} else {
const correctPassword = await argon2.verify(userRecord.password, password);
if (!correctPassword) {
throw new Error('Incorrect password')
}
}
return {
user: {
email: userRecord.email,
name: userRecord.name,
},
token: this.generateJWT(userRecord),
}
}
}
复制代码
这里密码认证使用了 argon2 库来防止时序攻击(timing-based attacks),也就是说,当攻击者试图靠蛮力破解口令时须要严格遵循服务器响应时间的相关准则。
接下来咱们将讨论一下如何生成 JWT。
一个 JSON Web Token or JWT 是一个以字符串或者 Token 形式存储的、通过编码的 JSON 对象。
你能够认为它是 cookie 的替代者。
Token 有下面三个部分(不一样颜色标注)
JWT 中的数据能够无需**密钥(Secret)或签名(Signature)**在客户端解码。
所以对于用户角色信息、配置文件、令牌过时时间等这些前端领域常见的信息或元数据(metadata)来讲,编码在 JWT 中一块儿传输就变得很方便。
咱们实现一个 generateToken 方法来完善咱们的认证服务程序吧。
经过使用 jsonwebtoken
这个库(你能够在 npmjs.com 找到它),咱们就能建立一个 JWT 了。
import * as jwt from 'jsonwebtoken'
class AuthService {
private generateToken(user) {
const data = {
_id: user._id,
name: user.name,
email: user.email
};
const signature = 'MySuP3R_z3kr3t';
const expiration = '6h';
return jwt.sign({ data, }, signature, { expiresIn: expiration });
}
}
复制代码
重要的是,永远不要在编码数据中包含用户的敏感信息。
上面 signature 变量其实就是用来生成 JWT 的密钥(secret),并且你要确保这个 signature 不会泄漏出去。
若是攻击者经过某种方法获取了 signature,他就能生成令牌而且假装成用户从而窃取他们的会话(session)。
如今,前端须要在每一个请求中带上 JWT 才能访问到安全目标(secure endpoint)了。
一个比较好的作法是在请求的 header 中附带 JWT,一般是 Authorization 消息头(Authorization header)。
如今,咱们须要在后端中建立一个 express 的中间件。
中间件 isAuth
import * as jwt from 'express-jwt';
// 咱们假定 JWT 将会在 Authorization 请求头上,可是它也能够放在 req.body 或者 query 参数中,你只要根据业务场景选个合适的就好
const getTokenFromHeader = (req) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
}
export default jwt({
secret: 'MySuP3R_z3kr3t', // 必须和上一节的代码的 signature 同样
userProperty: 'token', // this is where the next middleware can find the encoded data generated in services/auth:generateToken -> 'req.token'
getToken: getTokenFromHeader, // 从 request 中获取到 auth token 的方法
})
复制代码
建立一个能从数据库中获取到完整用户记录的中间件,而且将这些用户信息放进 request 中。
export default (req, res, next) => {
const decodedTokenData = req.tokenData;
const userRecord = await UserModel.findOne({ _id: decodedTokenData._id })
req.currentUser = userRecord;
if(!userRecord) {
return res.status(401).end('User not found')
} else {
return next();
}
}
复制代码
如今就能够跳转到用户请求的路由了
import isAuth from '../middlewares/isAuth';
import attachCurrentUser from '../middlewares/attachCurrentUser';
import ItemsModel from '../models/items';
export default (app) => {
app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => {
const user = req.currentUser;
const userItems = await ItemsModel.find({ owner: user._id });
return res.json(userItems).status(200);
})
}
复制代码
通过两个中间件访问到的 inventory/personal-items 路由就是安全的。你须要有效的 JWT 才能访问这个路由,固然喽,路由也须要 JWT 中的用户信息才能去数据库中正确查找相应的信息。
你读到这里,一般会想到这么一个问题:
Q:若是能够在客户端中解码 JWT 数据的话,别人可否修改其中用户 id 或者其它的数据呢?
A:虽然你能够轻易地解码 JWT,可是没有 JWT 生成时的密钥(Secret)就没法对修改后的新数据进行编码。
也是由于这个缘由,千万不要泄漏密钥(secret)。
咱们的服务端会在 IsAuth
这个使用了 express-jwt
库的中间件中校验密钥。
如今咱们已经明白了 JWT 是如何工做的,咱们接下来去看一个很酷的功能。
用户模拟是一种能够在无需用户密码的状况下,以一个特定用户的身份登陆的技术。
对于超级管理员(super admins)来讲,这是一个很是有用的功能,可以帮他解决或调试一个仅会话可见的用户的问题。
没有必要去知道用户的密码,只须要以正确的密钥和必要的用户信息来建立一个 JWT 就能够了。
咱们来建立一个路径,来生成模拟生成特定用户登陆的 JWT。这个路径只能被超级管理员帐户使用。
首先,咱们须要为超级管理员建立一个更高等级的角色,方法有不少,比较简单的一种就是直接去数据库中给用户记录添加一个“role”字段。
而后,咱们建立一个新的中间件来检查用户角色。
export default (requiredRole) => {
return (req, res, next) => {
if(req.currentUser.role === requiredRole) {
return next();
} else {
return res.status(401).send('Action not allowed');
}
}
}
复制代码
这个中间件须要放在 isAuth
和 attachCurrentUser
以后。
最后,这个路径将会生成一个可以模拟用户的 JWT 。
import isAuth from '../middlewares/isAuth';
import attachCurrentUser from '../middlewares/attachCurrentUser';
import roleRequired from '../middlwares/roleRequired';
import UserModel from '../models/user';
export default (app) => {
app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => {
const userEmail = req.body.email;
const userRecord = await UserModel.findOne({ email: userEmail });
if(!userRecord) {
return res.status(404).send('User not found');
}
return res.json({
user: {
email: userRecord.email,
name: userRecord.name
},
jwt: this.generateToken(userRecord)
})
.status(200);
})
}
复制代码
因此,这里并无什么黑魔法,超级管理员只须要知道须要被模拟的用户的Email(而且这里的逻辑与登陆十分类似,只是无需检查口令的正确性)就能够模拟这个用户了。
固然,也正是由于不须要密码,这个路径的安全性就得靠 roleRequired 中间件来保证了。
虽然依赖第三方认证服务和库很方便,节约了开发时间,可是咱们也须要了解认证背后的底层逻辑和原理。
在这篇文章中咱们探讨了 JWT 的功能,为何选择一个好的加密算法很是重要,以及如何去模拟一个用户,若是你使用的是 passport.js 这样的库,就很难作到这些事。
在本系列的下一篇文章中,咱们将探讨经过使用 OAuth2 协议和更简单的替代方案(如 Firebase 等第三方用于身份验证的库)来为客户提供“社交登陆”身份验证的不一样方法。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。