nodejs使用session进行登录认证

基于Token 认证和session 认证的比较

前言:本文解决的问题

  • 基于session 认证的不足
  • 基于 token 认证的过程
  • session VS tooken

1.传统基于Cookie 认证的过程

长期以来,基于Session的认证(Session based authentication)一直处于主流地位。由于http协议是无状态的,借助cookie,客户端登陆成功后,服务端就能识别其后续请求,而不需要每次都登陆。它是有状态的(statefull),也就是服务端和客户端都需要保存生成的session*,也就是说在服务端需要在数据库中追踪session是否alive,客户端要把session写入cookie中。基本过程如下:

  • 客户端登陆,一般输入用户名和密码
  • 服务端如果验证通过,就会生成session,并把它存入数据库中
  • 客户端在浏览器上会产生cookie,并把session写入
  • 客户端后续有新的请求,都会在请求后携带sessIon,发给服务端
  • 如果客户端登陆出去(log out),该生成的session就会在客户端和服务端都被销毁
    如下图:


    image

基于Session 认证的不足
正如图片所示,服务端需要保存每个用户的session,这对于很多访问用户的情景来说,服务端的负担很重,需要大量的的资源来存储session;另一方面不能很好解决跨域资源共享问题Cross-Origin Resource Sharing (CORS)。最重要的是cookie的使用引入了很多不安全因素,招致了很多专门针对cookie的攻击。

2. 基于token的认证

近年来,基于token的认证开始成为主流,不管是单个网页,还是web app,还是Internet of Things 。该认证方式是无状态的(stateless),客户端登陆成功后,服务端会生成一个token并把它返还给客户端,由于是无状态的,服务端不再保存该Token**。

问题来了,当客户端再发送请求时,服务端如何判断它曾经已经登陆了?类似Sesssion based authentication,客户端每次发送请求时也会携带Token。由于这里的Token是服务端用自己的密钥签名的,当它受到客户但的Token时,只需要再用自己的密钥去验证,就可以判断这个Token是不是刚自己签发的。这里面的核心就是用签名和验证,从而减轻了服务端的负担,无需再存储session,尤其是对于大型的分布式应用,减负很多。以下是具体的过程:

  • 客户端用自己的机密信息登陆,如用户名和密码
  • 服务端验证,验证通过,生成Token返还给客户端(一般用哈希算法,再加个随机数)。
  • 客户端把Token写入local storage(本地内存),后续请求都携带该Token
  • 服务端收到请求时验证Token,如果验证通过,则允许用户访问相应资源


    token based authentication

问题

由于这里Token是后续用来登陆的唯一认证手段,如果用户关闭了网页,被其它别有用心的应用窃取到Token,拿它再来登陆就危险了。因此这里需要服务端给Tooken设置过期时间(expired time),不能太长,太长不安全;太短用户体验差。另外,当用户登出时,服务端要把当前Token设为黑名单(back list),防止被冒用。

3. 基于token的优点

  • 无状态,stateless

服务端无需保存生成的Token,它需要做的就是签发Token,并验证它。由于Token中是含有需要验证的所有信息(签名算法、用户信息、签名),这就让服务端负载减轻了很多

  • 交叉域和交叉域共享Cross-domain / CORS

Cookie和CORS在不同的域效果不好。而使用基于Token可以使API应用到不同的服务和域中。

  • 可以在JWT中存储数据 Store Data in the JWT

我们知道,现通用的Token based Authentication 就是JOSN Web Token(jwt)。JWT包含头部(header),载荷(claim set), 和签名(signature)。可以在载荷中存放预定义的元数据,只要是JOSON格式就可以了。

  • 不需要CSRF(cross-site-request-forgery)防护

由于不需要依赖Cookie,自然不用担心Cookie被截获,用户信息被伪造登陆问题。

代码补充

由于某系逻辑不需要故进行了删减。详细可参考注释信息。

4. 使用nodejs实现session

4.1. 获取cookie并写入session

//www.js
const http = require("http")
const PORT = 3000

const querystring = require('querystring')
const serverHandle = require("../app")
//创建服务,处理请求
const server = http.createServer(serverHandle)

server.listen(PORT, () => {
  console.log('listening on PORT' + PORT)
})
//app.js
//...
let SESSION_DATA ={}
//JavaScript对象是存储在内存的堆中的,只要该进程不被重启,
//或使用某种淘汰算法淘汰其中的某些值,那么这个对象中的值和对象将长时间存在。


//当某一个请求进来后,下面的函数被激活,每个链接都会获取当前请求的cookie和sessionID
//如果cookie中没有sessionID,则生成一个sessionID并在SESSION_DATA中添加
//因此当浏览器在首次请求时,服务器仍然会返回一个sessionID。
const handleServer = (req,res)=>{
/...
//获取cookie
req.cookie = {}
  const cookieStr = req.headers.cookie || ''
  cookieStr.split(';').forEach(item => {
    if (!item) {
      return
    }
    const arr = item.split('=')
    const key = arr[0].trim()
    const val = arr[1].trim()
    req.cookie[key] = val
  });

//解析session
  let userId = req.cookie.userid //判断请求cookie中是否有被添加userid(sessionID)
  let needSetCookie = false //是否需要set-cookie
  if (userId) {//cookie中有userid(sessionID)(之前set-cookie过)
    if (!SESSION_DATA[userId]) {//服务器内存中不存在该session(可能过期)
      SESSION_DATA[userId] = {}//使用该sessionID初始化一个新的session
    }
  }
  else {//客户端未被添加过sessionID
    needSetCookie = true
    userId = (Date.now() + (Math.random() * 100).toFixed(0)) + ''//生成sessionID
    SESSION_DATA[userId] = {}//初始化session
  }
//设置req.session 指向全局的SESSION_DATA中的某一条session(也就是刚才生成的或者cookie中sessionID相同的的那个session)
  req.session = SESSION_DATA[userId]
}
//......

4.2. 登录,向session中添加信息。

//controller中用于处理登录的方法
const { PostLogin } = require('../controller/userController')

const { SuccessMoudle, ErrorMoudle } = require('../moudle/resMoudle')
//设置过期时间
const setCookieExpries = () => {
  const d = new Date()
  d.setTime(d.getTime() + (24 * 60 * 60 * 1000))
  return d.toGMTString()
}

const handleUserRouter = (req, res) => {
  const method = req.method
  const url = req.url
  const path = url.split('?')[0]
  //使用GET模拟登录,不安全。
  if (method === 'GET' && path === '/api/user/login') {
    const { username, password } = req.query
    const result = PostLogin(username, password)

    return result.then(isSussful => {
      //登录成功
      if (isSussful) {
      //向req.session.username中添加信息,由于req.session 之前设置为指向SESSION_DATA中的对应session,
      //修改或向req.session对象中添加属性会对SESSION_DATA[sessionID]起作用。
      //所以修改req.session会引起全局的SESSION_DATA变化
        req.session.username = username
        return new SuccessMoudle()
      } else {
        return new ErrorMoudle('登录失败')
      }
    })

  }
//app.js
///...
 const userData = handleUserRouter(req, res)
    console.log(SESSION_DATA)
    if (userData) {
      if (needSetCookie) {
//在处理完每个请求后都应该向cookie中添加信息
        res.setHeader('Set-Cookie', `userid=${userId};path=/;httpOnly;expires=${setCookieExpries()}`)
       
      }
      userData.then(result => {
        res.end(
          JSON.stringify(result)
        )
      }
      )
      return
    }

4.3. 验证登录状态

  //验证登录状态
  if (method === 'GET' && req.path === '/api/user/login-test') {
    if (req.session.username) {//判断SESSION_DATA[sessionID]中是否存在username属性,之前登录成功时添加过属性
      return Promise.resolve(new SuccessMoudle(req.session))
    } else {
      return Promise.resolve(new ErrorMoudle("尚未登陆"))
    }
  }
}

module.exports = handleUserRouter

你可能感兴趣的:(nodejs使用session进行登录认证)