koa2验证客户端身份的三种方法(标准)

503 阅读5分钟

http协议是无状态协议,即协议对于处理没有记忆能力。而且每次连接只处理一个请求。服务器处理完客户的请求,并收到客户端的应答后,断开连接。

所以,服务器端需要使用一种手段来验证客户端的身份,并且还要预防黑客的攻击

前两种:

使用 cookie 和 session 的方式:

const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();


// cookie和session
const koaSession = require('koa-generic-session');

const session = koaSession({
    key: 'sessionid',
    maxAge: 1000, /** (number) maxAge in ms (default is 1 days),cookie的过期时间 */
  	overwrite: true, /** (boolean) can overwrite or not (default true) */
  	httpOnly: true, /** cookie是否只有服务器端可以访问 (boolean) httpOnly or not (default true) */
  	signed: true /** 加密? */
}, app)

// 加盐操作:在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
app.keys = ['aaa', 'bbb'] // 数组形式

router.get('/text/login', function (ctx, next) {
    // 在服务器为登录的客户端,设置一个加密的cookie (服务器自动为浏览器保存一个cookie,每次请求都会带上cookie)
	ctx.session.name = 'ikun'
    ctx.body = '登录成功~'
})
router.get('/text/list', function (ctx, next) {
	const value = ctx.session.name
    if(value === 'ikun') ctx.body = 'userList~'
    ctx.body = '没有权限,先登录'
})

app.use(session) // 存入中间件


app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7788,()=>{
	console.log('starting at port 3000');
});

image.png

解释:
  • 上图可以发现:直接在浏览器通过url访问(客户端没有执行其他操作),服务端自动设置了cookie(pc端),在下一次请求会自动携带该cookie

    • 使用了加盐模式,会出现俩个对应的 sessionidsessionid.sig,黑客需要同时破解才可以伪造信息
    • 而且可以发现,在其他页面,这里的cookie也是存在着,这是区分不同页面的标识
  • cookie和session缺点

    • cookie会被附加在每一个 HTTP 请求中,无形增加了流量(某些请求不需要)

    • cookie是明文传递的,存在安全性问题

    • cookie的大小限制是 4kb,对于复杂需求是不够的

    • 对于pc端浏览器可以自动保存,但是对于移动端需要自己手动设置cookie和session

    • 对于分布式系统和服务器集群中保证不同系统正确解析session的困难性

      • 解决高并发出现的不同系统

第三种

token

使用 token (令牌)验证身份

  • 验证用户账号密码正确情况,给用户颁发一个令牌(可以作为后续用户访问接口的有效凭证)
  • 登录生成颁发token,访问某些接口验证token
JWT 实现 token 机制

三部分组成

  • header

    • alg:采用的加密算法,默认是 HMAC SHA256(HS256),采用一个密钥进行加密和解密(对称加密)
    • typ:JWT(JSON Web Token),固定值,写 JWT 即可
    • 会通过base64算法进行编码
  • payload

    • 携带的数据,比如我们可以将用户的id和name放到payload中
    • 默认也会携带 iat(issued at),令牌签发时间
    • 设置过期时间 exp(expiration time)
    • 会通过base64 算法进行编码
  • signature

    • 设置一个 secretKey,通过将前两个结果合并后进行 HMAC SHA256的算法
    • HMAC SHA256(base54Url ( header ) + . + base64Url ( payload ), secretKey)
    • secretKey不可暴露(暴露就可以模拟颁发token,也可以解密token)

代码实现:(对称加密)

const Koa = require('koa');
const router = require('koa-router')();
const jwt = require('jsonwebtoken') // jwt鉴权
const app = new Koa();

const secretkey = 'aabbcc' // 不可以暴露

router.get('/text/login', function (ctx, next) {
    const payload = {id: 111, name: 'lhy'}
    const token = jwt.sign(payload, secretkey, {
        expiresIn: 60
    })
    ctx.body = {
        code: 0, token, message: '登陆成功'
    }
})
router.get('/text/list', function (ctx, next) {
	const authorization = ctx.headers.authorization // 一般设置将token存在请求头的headers.authorization中
    const token = authorization.replace('Bearer', '')
    try {
        const result = jwt.verify(token, secretkey) // 验证,如果验证失败会throw error(invalid token)
        
        ctx.body = {code: 0, data: [1,2,3]}
    } catch(e) {
        ctx.body = {code: -1010, message: '无效token'}
    }
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7788,()=>{
	console.log('starting at port 3000');
});
注意:

有安全性问题:对于分布式系统或者服务器集群,如果黑客攻击某一个子系统获得了 secretkey 值,而且不同系统用的是同一套加密算法(对称加密),则可以通过伪造系统给用户颁发 token。

解决:
  • 用非对称加密(RS256):
    • 私钥:private_key发布令牌
    • 公钥:public_key验证令牌
    • 公钥和私钥一般都是直接通过命令生成的
  • 父系统用 私钥 颁发token(父系统有充足安全措施,不怕被黑客攻击),子系统用 公钥 验证(黑客攻击获得公钥,无法伪造颁发token)

    • 在 linux 或者 Max 系统可以直接打开终端
    • 在 windows 系统需要用 git bash 打开openssl
  • 使用(windows 系统):

    • 新建 keys 文件夹,用git bash 打开输入:
    openssl
    # 会进入 OpenSSL>
    ​
    genrsa -out private.key 2048
    # 生成秘钥(.key结尾) 大小2048
    # secretOrPrivateKey has a minimum key size of 2048 bits for RS256
    ​
    rsa -in private.key -pubout -out public.key
    # 生成公钥
    

image.png

具体实现:

// token非对称加密
const fs = require('fs') // './' 返回你执行node命令的路径
const path = require('path')
// __dirname总是指向被执行js文件的绝对路径,在/d1/d2/1.js文件中写了__dirname,它的值就是/d1/d2
const privateKey = fs.readFileSync(path.resolve(__dirname,'keys/private.key')); // 同步读取
const publicKey = fs.readFileSync(path.resolve(__dirname,'keys/public.key')); // 读取到的是一个buffer二进制流

router.get('/text3/login', function (ctx, next) {
  const payload = {id: 111, name: 'lhy'}
  const token = jwt.sign(payload, privateKey, { // token接受buffer二进制流
      expiresIn: 60,
      algorithm: 'RS256' // 默认是HA256对称加密,需要改为非对称加密
  })
  ctx.body = {
      code: 0, token, message: '登陆成功'
  }
})
router.get('/text3/list', function (ctx, next) {
const authorization = ctx.headers.authorization
  const token = authorization.replace('Bearer', '')
  try {
      const result = jwt.verify(token, publicKey, {
        algorithm: ['RS256'] // 传入数组,解密失败用下一种算法解密
      }) // 用公钥验证
      ctx.body = {code: 0, data: [1,2,3]}
  } catch(e) {
      ctx.body = {code: -1010, message: '无效token'}
  }
})