深入理解cookie 、session 和 token

1,393 阅读6分钟

cookie session原由

由于 http 是无状态的协议,一旦客户端和服务器的数据交换完毕,就会断开连接,再次请求,会重新连接,这就说明服务器单从网络连接上是没有办法知道用户身份的。为了解决这个问题,就给每次新的用户请求时,发一个身份证,每次访问都要带上身份证,这样服务器就知道是谁来访问了,针对不同的用户做出不同的响应。

会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是CookieSessionCookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

cookie是什么?

cookie是服务端发送到用户浏览器存储在客户端的状态,它会在浏览器下次向同一服务器发起请求时,被自动携带到服务器上。

以node为例设置cookie

const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const app = new Koa();
const router = new Router();
app.use(static(__dirname,'/'))

router.get('/',async ctx=>{
	//观察请求cookie存在
    console.log('cookie:',ctx.header.cookie)
    //设置cookie
    ctx.set('Set-Cookie','cookie1=1234')
})

app.use(router.routes())
app.listen(3000,() => {
    console.log('3000端口已开启');
})

session是什么?

session会话机制是一种服务器端机制,这里我们想象一下,如果将用户的一些信息都存入Cookie中的话,一旦信息被拦截,那么我们所有的用户信息都会丢失掉。所以就出现了Session,在一次会话中将重要信息保存在Session中,Cookie只记录SessionId一个SessionId对应一次会话请求。

session实现原理

  • 服务器在接受客户端首次访问时在服务器端创建session,然后保存session(我们可以将session保存在内存中,面对服务器集群我们需要将session保存在redis中),然后给这个session生成一个唯一的标识sessionId,然后在响应头种下sessionId
  • 签名,这一步通过后端密钥对sessionId进行签名处理,避免客户端修改sessionId
  • 浏览器收到请求响应的时候解析响应头,然后将sessionId保存在cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息。
  • 服务器在接受客户端请求时会去解析请求头cookie中的sessionId,然后根据这个sessionId在服务端查询该用户信息

koa-session

在koa中使用session,就要用到koa-session中间件 npm i koa-session -S

const Koa = require('koa')
const router = require('koa-router')()
const session = require('koa-session')
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa();

const redisStore = require('koa-redis');
const redis = require('redis');
const redisClient = redis.createClient(6379,"localhost")

const wrapper = require('co-redis')
const client = wrapper(redisClient)

//签名key keys作用用来对cookie进行签名
app.keys = ['some secret'];
//配置session的中间件
const SESS_CONFIG = {
    key: 'kkb:sess', // 名
    maxAge: 8640000, // 有效期
    httpOnly: true, // 服务器有效
    signed: true // 签名
    store: redisStore({ client })	//session保存到redis
}

app.use(static(__dirname + '/'));

app.use(bodyParser())
app.use(session(SESS_CONFIG,app));

app.use((ctx, next) => {
    if (ctx.url.indexOf('login') > -1) {
        next()
    } else {
        console.log('session', ctx.session.userinfo)
        if (!ctx.session.userinfo) {
            ctx.body = {
                message: "登录失败"
            }
        } else {
            next()
        }
    }
})

router.post('/login', async (ctx) => {
    const {
        body
    } = ctx.request
    console.log('body',body)
    //设置session
    ctx.session.userinfo = body.username;
    ctx.body = {
        message: "登录成功"
    }
})
router.post('/logout', async (ctx) => {
    //删除session
    delete ctx.session.userinfo
    ctx.body = {
        message: "登出系统"
    }
})
router.get('/getUser', async (ctx) => {
    ctx.body = {
        message: "获取数据成功",
        userinfo: ctx.session.userinfo
    }
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000,()=>{
    console.log('3000端口已开启')
});

这里为什么要将session存储在redis?

kos-session存在弊端,session信息未加密存储在客户端cookie中,浏览器cookie有长度限制,所以我们要将session存储在外部存储中。

上述session-cookie方式是个怎样的过程?

  • 用户登录的时候,服务端生成一个唯一的会话标识,并以它为key存储数据
  • 会话标识在客户端和服务端之间通过cookie进行传输
  • 服务端通过会话标识可以获得会话相关信息,然后对客户端的请求进行响应如果找不到有效的会话,那么认为用户是未登录状态
  • 会话会有过期时间,也可以通过一些操作(比如登出)来主动删除

token是什么?

Token 是访问资源接口(API)时所需要的资源凭证。

token与session有哪些不同?

  • session要求服务端存储信息,并且能够根据id进行检索,而token不需要(因为信息就在token中,这样就实现了服务器无状态化)。在大规模系统中,对每个请求都检索会话信息可能是一个复杂和耗时的过程。但另外一方面服务端要通过token来解析用户身份需要定义好相对应的协议(比如JWT)
  • session一般通过cookie来交互,而token方式更灵活,可以是cookie、header,也可以放在请求的内容中。不使用cookie可以带来跨域的便利性
  • token生成的方式更加多样化,可以由第三方模块来提供
  • token若被盗用,服务端无法感知,cookie信息存储在用户自己电脑上,被盗用风险略小

token原理

  • 客户端使用用户名和密码请求登录
  • 服务端收到请求,去验证用户名和密码
  • 验证成功后,服务端会签发一个令牌(Token),再把这个令牌发送给客户端
  • 客户端收到Token以后,把它存储起来,比如放在cookie或者localStore
  • 客户端每次向服务端请求资源的时候需要带着服务器签发的Token
  • 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据

利用koa模拟接口

安装依赖 npm i jsonwebtoken koa-jwt -S

后端代码:


const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors')
const jwt = require('jsonwebtoken')
const jwtAuth = require('koa-jwt')

// 后端密钥
const secret = 'balabal ssadhd'
const app = new Koa();
const router = new Router();

app.use(static(__dirname + '/'))
app.use(bodyParser())

router.post('/login-token',async ctx => {
    const {body} = ctx.request;
    const userinfo = body.username;
    ctx.body = {
        message:'登录成功',
        user:userinfo,
        token:jwt.sign( //生成token
            {
                //用户信息
                data:userinfo,
                // 设置token有效期
                exp:Math.floor(Date.now() / 1000) + 60 * 60
            },
            secret
        )
    }
});

router.get('/getUser-token',jwtAuth({secret}),async ctx => {
    // 验证token通过
    console.log(ctx.state.user)
    ctx.body = {
        message:'获取数据成功',
        userinfo:ctx.state.user.data
    }
})

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000,() => {
    console.log('3000端口已开启')
})

前端代码:

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <div id="app">
      <div>
        <input v-model="username" />
        <input v-model="password" />
      </div>
      <div>
        <button v-on:click="login">Login</button>
        <button v-on:click="logout">Logout</button>
        <button v-on:click="getUser">GetUser</button>
      </div>
      <div>
        <button @click="logs=[]">Clear Log</button>
      </div>
      <!-- 日志 -->
      <ul>
        <li v-for="(log,idx) in logs" :key="idx">
          {{ log }}
        </li>
      </ul>
    </div>
    <script>
      axios.interceptors.request.use(
        config => {
          const token = window.localStorage.getItem("token");
          if (token) {
            // 判断是否存在token,如果存在的话,则每个http header都加上token
            // Bearer是JWT的认证头部信息
            config.headers.common["Authorization"] = "Bearer " + token;
          }
          return config;
        },
        err => {
          return Promise.reject(err);
        }
      );

      axios.interceptors.response.use(
        response => {
          app.logs.push(JSON.stringify(response.data));
          return response;
        },
        err => {
          app.logs.push(JSON.stringify(response.data));
          return Promise.reject(err);
        }
      );
      var app = new Vue({
        el: "#app",
        data: {
          username: "test",
          password: "test",
          logs: []
        },
        methods: {
          async login() {
            const res = await axios.post("/login-token", {
              username: this.username,
              password: this.password
            });
            localStorage.setItem("token", res.data.token);
          },
          async logout() {
            localStorage.removeItem("token");
          },
          async getUser() {
            await axios.get("/getUser-token");
          }
        }
      });
    </script>
  </body>
</html>


让我们看一看实现过程

  • 用户登录的时候,服务端生成一个token返回给客户端
  • 客户端后续的请求头都带上这个token
  • 服务端解析验证token获取用户信息,并响应用户的请求
  • token会有过期时间,用户登出的时候会废弃token,但是服务端不需要任何操作

JWT是什么?

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,JWT代表着用户所有数据都保存在客户端,每次请求都发回服务器。

参考 阮一峰 JWT 入门教程