三、Sails 中使用Jwt进行身份认证

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,然后完善验证代码中验证不通过的提示信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值