前言
在大多数的应用里,身份验证是必须的。最近学习了基于token的身份验证,很多大型网站也在用,例如Facebook,twitter,Google,github。大家通常将token翻译为“令牌”。顾名思义,只有拿到这个令牌,你才能通关,才能去请求相应的数据或资源。
本文讲解了传统的和基于token的身份认证方式,并介绍了JWT相关知识,最后附上Node.js中使用jsonwebtoken、passport、passport-jwt模块获取和验证token的示例。
传统身份验证
HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名和密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。
解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,带着这个 Cookie 。服务端收到请求后,会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。
token身份验证
使用基于 Token 的身份验证方法,服务端不需要存储用户的登录记录。大概的流程如下:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
JWT
从上文可知,token验证即:通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器。JWT是实施 Token 验证的代表,读作:jot ,表示:JSON Web Tokens 。也可以称作JSON Web令牌
1. JWT的原则
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
{
"UserName": "Ciger",
"Role": "Admin",
"Expire": "2019-02-18 20:15:56"
}
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名
2. JWT的数据结构
JWT标准的Token有如下三个部分
- header (头部)
- payload (数据)
-
signature (签名)
这三部分用点分隔开,并且使用Base64url编码。
Token看起来如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjNjc3OTExMjdjYWEzMjFmNDAzOTc1MyIsIm5hbWUiOiJjY2MiLCJpYXQiOjE1NTAzMjAyNzcsImV4cCI6MTU1MDMyMzg3N30.a-CWZMdmtXnSJrYGUS6fbmqd4VzY6lPftDQ2A3UsK0w
Header
每个 JWT token 里面都有一个 header,也就是头部数据。里面包含了使用的算法,这个 JWT 是不是带签名的或者加密的。主要就是说明一下怎么处理这个 JWT token 。
头部里包含的东西可能会根据 JWT 的类型有所变化,比如一个加密的 JWT 里面要包含使用的加密的算法。唯一在头部里面要包含的是 alg 这个属性,如果是加密的 JWT,这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT,这个属性的值要设置成 none。
示例:
{
“alg”:"HS256"
}
意思是这个JWT用的算法为HS256。上面的json内容经过Base64url编码后如下:
eyJhbGciOiJIUzI1NiJ9
Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:
- iss:Issuer,发行者
- sub:Subject,主题
- aud:Audience,观众
- exp:Expiration time,过期时间
- nbf:Not before
- iat:Issued at,发行时间
- jti:JWT ID
示例:
{
"exp": "1438955445",
"name": "Ciger",
"admin": true
}
使用base64url编码后如下:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64Url 编码的 header.payload ,之后选择一个加密算法加密,将Secret作为参数放进去(这个Secret相当于一个密码,存储在服务端)
- header
- payload
- secret
const encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
加密后如下:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后将header、payload、signature三部分组合起来便是一个要发送给客户端的token
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客户端收到这个token之后将它存储起来,下次向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。
Node.js中使用Express结合Passport实现登陆认证
1. 首先安装
cnpm install jsonwebtoken
cnpm install passport-jwt passport
jsonwebtoken用于获取token,passport和passport-jwt用于验证token。passport是express框架中一个针对密码的中间件,而passport-jwt是一个针对jsonwebtoken的插件。
2. 得到token
在项目中添加一个.js文件,如users.js
const jwt = require('jsonwebtoken')
// Token 数据
const payload = {
name: 'Ciger',
admin: true
}
// 密钥
const secretOrKey = 'secret'
// 签发 Token
const token = jwt.sign(payload, secretOrKey , { expiresIn: '3600' })
// 输出签发的 Token
console.log(token)
jwt.sign()方法有三个参数:
- payload: token里面要包含的一些数据
- secret:签发token用的密钥,验证token时也要用到这个密钥
- options:一些其他选项,如过期时间expiresIn
在命令行下执行node users.js,就会输出应用签发的token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzM5MDYsImV4cCI6MTUyOTEyMDMwNn0.DctA2QlUCrM6wLWkIO78wBVN0NLpjoIq4T5B_2WJ-PU
2. 验证JWT
这里我们采用passport+passport-jwt来验证,你也可以采用其他方法验证,如jwt.verify
采用passport+passport-jwt方法验证,首先需要在入口文件中引入passport并初始化
入口文件-server.js
const passport = require('passport');
app.use(passport.initialize())
3. 对passport进行一些配置
在config文件夹下新建一个passport.js文件,然后在入口文件(server.js)中引入
require('./config/passport')(passport)
在passport.js中,引入以下模块:
- passport-jwt
- mongoose
- keys.js
- models/Users,js
具体代码如下:
const JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
const mongoose = require('mongoose')
const User = mongoose.model('users')
const keys = require('./key')
const opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;
module.exports = (passport) => {
passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id)
.then(user=>{
if(user){
//此处会将user信息写入req.user
return done(null,user)
}
return done(null,false)
}).catch(err=>{console.log(err)})
}))
}
(passport.js配置信息复制粘贴修改下即可)
4. 验证token
router.get("/current",passport.authenticate("jwt",{session:false}),(req,res) => {
res.json({
req.user
})
总结
token就像一个令牌,我们只有拿到这个令牌才能向服务器去请求用户的信息。Node.js中获取token可以用jsonwebtoken模块,验证token用passport和passport-jwt模块。对于密码可用bcrypt模块加密存储进数据库。