文章目录
Jwt 概述
由于我们是完全前后端分离的开发模式,我们的后端对前端提供Api,这个时候Api就会暴露出来,有些Api是包含敏感信息的。出于安全考虑的设计,肯定不允许任何人都可以访问这样的Api,这个时候就需要对Api的请求(request)进行身份认证(Authorization)。这种情况我们可以采用Jwt(Json Web Token)来完成这个事情。
为什么要用Jwt
身份认证还有一个可选项是采用Session技术,采用Jwt是因为它更适合前后端分离的开发模式。大致有几点原因:
- Jwt中的token存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
- Session存储在服务端,日后后端采用负载均衡自动选择服务器的时候无法进行同步认证。
- Session因为需要服务端存储,一般都要定期释放,如果要做类似“30天内记住密码”这种操作,那就会在服务器上面占用比较大的资源(30天内不能释放)。
- Session 对CSRF的防御显然不如token。
Jwt原理
你可以简单的把Jwt理解成一种加解密技术。它把待加密信息分成三个部分:
- Header 头部包含加密算法和toke类型
- Payload 负载包含要加密的数据
- Verify Signature 签名是我们可以保存在服务端的一个key字符串
构成这三个部分的信息经过Jwt加密成一个字符串,类似这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
我们的前端通过输入用户名+密码的方式给后端来实现登录,后端验证通过后把用户信息(一般是剔除掉密码的)和服务器的签名加密成一个字符串作为令牌(token)返回给前端。前端保存这个令牌,要访问敏感信息的时候发送给后端验证该token,如果签名是对的,并且token里面的用户信息也是对的,那我们就给予放行,否则就给予阻止。
关于Jwt的更多信息,https://jwt.io网站上有更详细的介绍,还有提供在线加解密的测试功能。
Jwt认证
安装 Jwt 库
在终端运行:
npm install jsonwebtoken --save
登录Api
安装Jwt之后,我们可以设计用户登录动作,我们再UserControllers控制器里面,添加函数login实现登录动作,代码如下:
export async function login(req: Api.SailsRequest, res: Api.Response): Promise<any> {
let email = req.body.email, password = req.body.password;
if (!(email && password)) { return res.serverError('email或password出现空值,请检查。') }
//获取sails数据模型里面的user并且转换为UserModel.UserInstance类型
let User = <UserModel.UserInstance>sails.models.user;
try {
User = await User.findOne({ email: req.body.email }).decrypt();
if (!User) return res.status(401).send(`找不到email为:${req.body.email}的用户。`);
if (User.password !== password) return res.status(401).send(`用户密码错误。`);
let token = jwt.sign(
User.toJSON(),//需要调用toJSON转为简单对象(plain object)并去除密码,不能使用包含function的对象
sails.config.models.dataEncryptionKeys.default,
//sails.config.models.dataEncryptionKeys.default 采用config/models里面的数据加密密钥,避免多个密钥设置
{ expiresIn: '7d' }
//expiresIn 解释 1h:1小时 1m:1分钟 1s:1秒 1d:1天 100:100毫秒 详见:https://github.com/vercel/ms
);
return res.send(token);
} catch (error) {
return res.serverError(error);
}
}
Verify Signature
前面我们知道Jwt的Payload就是我们需要保存的数据,这个通过jwt.sign函数的第一个参数Uset.toJSON()获取,需要注意的是验证的签名需要保持在服务器,Sails里面本来就有对设置为encrypt的字段进行加密的功能,这个加密也是需要提供私钥的,我们可以通过config/model.js里面的dataEncryptionKeys进行设置,如下:
module.exports.models = {
dataEncryptionKeys: {
default: '4lB56JWieHhJISbwG0r+Y9MlPLEyTREu+y+V4ViPasA='
},
}
为了避免后端维护多个密钥,在Jwt中我们可以采用同一个密钥。因此我们再源代码里面传入sails.config.models.dataEncryptionKeys.default作为Jwt的验证签名
过期时间
Jwt的过期时间采用的是https://github.com/vercel/ms这个库里面的时间描述,简单的理解就是d,h,m,s,分别代表天,分钟,小时和秒,如果是纯数字那就是毫秒为单位,其它表示方式可以参考该网站Readme
Nodejs 单线程易崩问题
我们采用Nodejs作为后端开发,常常会遇到Nodejs单线程迷思。实际上就是比较害怕线程安全问题,那么我们一开始Coding的时候就要紧绷这根弦,时刻注意开发中要及时采用try/catch来捕捉代码错误,防止因为我们没有捕获到的Error导致服务器卡死(uncaughtException die) 。
还有一些错误是从Node底层抛出的,有些异常 try/catch和uncaughtException都无法捕获。针对这种情况,行业内还提供forever这类守护进程的选项,让NodeJS遭遇异常崩溃以后能马上复活。 如果有需要可以安装使用。npm install forever -g
验证程序
用户登录(login)之后就拥有了token,访问端需要有可以对token进行验证的程序,我们在api/policies里面添加authenticated.ts,代码如下:
import Api from "typing/Api";
import UserModel from "typing/UserModel";
var jwt = require("jsonwebtoken");
declare var sails:any;
async function authenticated(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
var bearerToken;
var bearerHeader = req.headers['authorization'];
if (!bearerHeader ) return res.status(403).send('您没有权限访问本页面');
var bearer = bearerHeader.split(" ");
if(bearer.length<2) return res.status(401).send('authorization格式错误');
if (bearer[0] !== "Bearer") return res.status(401).send('authorization应该以Bearer为开头');
bearerToken=bearer[1];
try {
var decoded = await jwt.verify(bearerToken, sails.config.models.dataEncryptionKeys.default);
let User = <UserModel.UserInstance>sails.models.user;
User=await User.findOne({ id: decoded.id });//进一步查找用户信息
if(!User) return res.status(403).send('找不到用户信息');
req.user=User;
next();//放行(安检通过,执行next函数)
return true;
} catch(err:any) {
if(err.name=='TokenExpiredError'){
return res.status(403).send('登录超期,请重新登录后再试');
}
return res.status(401).send(err);
}
}
export = authenticated
本程序先解析request的请求头(header),如果没有authorization直接返回403,然后在解析authorization里面的Bearer,这种做法是行业内的通用做法,我们依例而做有利于我们程序通用性。验证的时候依然获取sails.config的dataEncryptionKeys作为签名,如果前端传来的签名不对,验证就不用通过,这种错误我们直接通过try/catch抛出去给前端即可。
修改配置
由于并不是所有api控制器里面的action都是需要身份验证的,比如login就不能进行验证,因为这个就是给用户登录的,用户登录前是不可能有token的。这个时候我们需要通过某个应用拦截器把api请求拦截住,检查看看有没有合格的身份验证,再决定是否放行(就像机场的安检),关于这样的拦截器Sails的机制是通过config配置hook来实现。在安全策略上面,我们可以通过config/policies.js中,设置需要验证用户登录的方法(controller中的action)来实现。修改改文件如下:
积极策略
module.exports.policies = {
UserController: {
retrieve: 'authenticated'
}
};
以上采用积极设置策略,设置UserController中的retrieve动作(UserController里面的retrieve方法)需要验证,验证的代码为authenticated (sails会到api/policies文件夹里面按照文件名称去找对应的程序)。其它没有设置的控制器里面的Action都是不需要进行验证的。
采用积极策略的验证方法,还需要考虑sails的blueprint API(蓝图),因为蓝图api是默认是隐藏的,不需要再config/routes.js里面列出来的,如果我们采用蓝图api,我们还需要一样对用到的api进行策略设置(采用消极设置的方式就不需要考虑这个),关于蓝图api我们后面还会再讲。
消极策略
还有一种设置测试是先把所有控制器里面的动作都设置为需要验证,除了某些控制器的某些Action,比如:以下设置为除了login登录动作之外都需要身份验证
module.exports.policies = {
'*': 'authenticated',
'UserController': {
'login': true
}
};
多重验证
有一些Api动作是需要多重验证的,比如需要验证登录,并且还需要是指定区域的用户才能访问,这样的验证可以设置如下:
'UserController': {
'getEncryptedData': ['isLoggedIn', 'isInValidRegion']
}
Jwt 测试
正常登录
运行node app.js之后,打开postman,根据大家在数据库中创建的用户信息,测试用户登录Api
把返回的token复制起来,postman中新开一个窗口,测试userController里面的retrieve控制器,不输入token的时候是这样的:
在Authorizaiton里面输入token是这样的:
这个时候,我们如果查看postman的请求,可以看到类似如下代码:可以看到请求的header里面是有token的,并且以Bearer开头
var config = {
method: 'post',
url: 'http://localhost:1898/api/userRetrieve',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkQXQiOjE2Njg4NDYyMzE2MzcsInVwZGF0ZWRBdCI6MTY2ODg0NjIzMTYzNywiaWQiOjEsImVtYWlsIjoienpjYWlzbTlAMTk5LmNvbSIsIm5pY2tuYW1lIjoiIiwiaWF0IjoxNjY5MjE0MzgzLCJleHAiOjE2Njk4MTkxODN9.ZqBXznUz4uZKOz1J7V8u0f_t9ibwh1YDtnTtpKjQpNk',
'Cookie': 'sails.sid=s%3A3AVeBjcS3gKYQ60SyqsuQVsmVTaj6tD1.RiFrw%2FEEJwOjoWnIGsKIJUqbHTXWLIlgkTGpzgVMpFU'
},
data : data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
})
过期或错误密钥测试
可以到jwt.io上面复制一段非服务器密钥加密过的token进行测试,Api返回invalid signature 。
也可以修改源代码里面的过期时间,做一个过期时间比较短的token出来测试。或是把我们加密过的token粘贴到jwt.io中,查看解密情况。这些操作将有助于提高您对jwt加密的认识。
通过测试,我们可以找出各种验证不通过的时候jwt捕获的错误消息中的name,然后完善验证代码中验证不通过的提示信息。