nodejs鉴权

三种常见鉴权方式

  • Session/CookieToken
  • OAuth
  • SSO
session-cookie方式
//cookie原理解析
// cookie.js
const http = require("http")
http.createServer((req, res) => {
        if (req.url === '/favicon.ico') {
            res.end('')
            return
        }
        // 观察cookie存在
        console.log('cookie:', req.headers.cookie) // 设置cookie
        res.setHeader('Set-Cookie', 'cookie1=abc;') 
        res.end('hello cookie!!')
    })
    .listen(3000)
set-cookie.png

由于cookie的明文传输,而且前端很容易篡改,不是很安全,另外cookie是有容量限制的,因此可以存储一个编号,编号对应的内容就可以放在服务器端。

const session = {}
//...
  if (req.url === '/favicon.ico') {
        res.end('')
        return
    }
    // 观察cookie存在
    console.log('cookie:', req.headers.cookie) // 设置cookie
    const sessionKey = 'sid'
    const cookie = req.headers.cookie
    if (cookie && cookie.indexOf(sessionKey) > -1) {
        res.end('Come Back ')
        // 简略写法未必具有通用性
        const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`)
        const sid = pattern.exec(cookie)[1]
        console.log('session:', sid, session, session[sid])
    } else {
        const sid = (Math.random() * 99999999).toFixed()
        // 设置cookie
        res.setHeader('Set-Cookie', `${sessionKey}=${sid};`)
        session[sid] = { name: 'laowang' }
        res.end('Hello')
    }
//...

session会话机制是一种服务器端机制,它使用类似于哈希表(可能还有哈希表)的结构来保存信息。

原理
image.png

实现原理: 1. 服务器在接受客户端首次访问时在服务器端创建seesion,然后保存seesion(我们可以将 seesion保存在内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一 个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。2. 签名。这一步通过秘钥对sid进行签名处理,避免客户端修改sid。(非必需步骤)3. 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息,4. 服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客户端的session,然后判断该请求是否合法。

koa中的session使用

koa是一个新的Web框架,致力于成为Web应用和api开发领域中的一个更小,更富有表现力,更健壮的基石,是express的下一代基于node.js的web框架 ,完全使用Promise并配合async来实现异步。
特点: 轻量 无捆绑 中间件架构 优雅的api设计 增强错误处理
// 安装: npm i koa koa-session -S

const Koa = require('koa')
const app = new Koa()
const session = require('koa-session')
// 签名key keys作用 用来对cookie进行签名 
app.keys = ['some secret'];
// 配置项
const SESS_CONFIG = {
    key: 'kkb:sess', // cookie键名 
    maxAge: 86400000, // 有效期,默认一天 
    httpOnly: true, // 仅服务器修改 
    signed: true, // 签名cookie
};
// 注册
app.use(session(SESS_CONFIG, app));
// 测试 app.use(ctx => {
app.use(ctx => {
    if (ctx.path === '/favicon.ico') return; // 获取
    let n = ctx.session.count || 0;
    // 设置
    ctx.session.count = ++n;
    ctx.body = '第' + n + '次访问';
});
app.listen(3000)

访问http://localhost:3000/

image.png

哈希Hash - SHA MD5
  • 把一个不定长摘要定长结果 -摘要 yanglaoshi -> x -雪崩效应

使用声明一个变量的方式存储session的这种方式,实际上就是存储在内存中,当用户访问量增大的时候,就会导致内存暴涨,而且如果服务器关机,那么驻留在内存中的session就会清空,第三点是服务器采用多机器部署,用户不一定每次都会访问到同一台机器,基于这三种情况我们需要把session保存在一个公共的位置,不能保存在内存中,这时候我们想到使用redis。

使用redis存储session

redis是一个高性能的key-value数据库,Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。
优势
  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
// npm install redis -S
// redis.js
const redis = require('redis');
const client = redis.createClient(6379, 'localhost');
client.set('hello', 'This is a value');
client.get('hello', function (err, v) {
    console.log("redis get ", v);
})
// koa-redis.js
const redisStore = require('koa-redis');
const redis = require('redis')
const redisClient = redis.createClient(6379, "localhost");
const wrapper = require('co-redis'); //为了在中间件中使用redisStore
const client = wrapper(redisClient);
app.use(session({
    key: 'kkb:sess',
    store: redisStore({ client }) // 此处可以不必指定client
}, app));
app.use(async (ctx, next) => {
    const keys = await client.keys('*')
    keys.forEach(async key =>
        console.log(await client.get(key))
    )
    await next()
})

为什么要将session存储在外部存储中,Session信息未加密存储在客户端cookie中浏览器cookie有长度限制

一个登录鉴权验证的小李子🌰

//index.js
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();

//配置session的中间件
app.use(cors({
    credentials: true
}))
app.keys = ['some secret'];

app.use(static(__dirname + '/'));
app.use(bodyParser())
app.use(session(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);
//index.html
<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 onclick="document.getElementById('log').innerHTML = ''">Clear Log</button>
    </div>
  </div>
  <h6 id="log"></h6>
  </div>
  <script>
    // axios.defaults.baseURL = 'http://localhost:3000'
    axios.defaults.withCredentials = true
    axios.interceptors.response.use(
      response => {
        document.getElementById('log').append(JSON.stringify(response.data))
        return response;
      }
    );
    var app = new Vue({
      el: '#app',
      data: {
        username: 'test',
        password: 'test'
      },
      methods: {
        async login() {
          await axios.post('/login', {
            username: this.username,
            password: this.password
          })
        },
        async logout() {
          await axios.post('/logout')
        },
        async getUser() {
          await axios.get('/getUser')
        }
      }
    });
  </script>
</body>
</html>
login.png

利用session要求服务器本身要有状态的,这样实现起来难度比较大的,最好是我们可以提供一种服务让后端可以没有状态,虽然我们现在使用redis加一个全局的状态保持统一,这样比较适合通过分布式系统进行实现,所以这是token产生的一个原因,现在实际在前端应用使用cookie-session的模式已经很少了,更多的是使用token模式

token验证
image

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

const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const bodyParser = require('koa-bodyparser')
const app = new Koa();
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");

const secret = "it's a secret";
app.use(bodyParser())
app.use(static(__dirname + '/'));

router.post("/login-token", async ctx => {
  const { body } = ctx.request;
  //登录逻辑,略
  //设置session
  const userinfo = body.username;
  ctx.body = {
    message: "登录成功",
    user: userinfo,
    // 生成 token 返回给客户端
    token: jwt.sign(
      {
        data: userinfo,
        // 设置 token 过期时间,一小时后,秒为单位
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      secret
    )
  };
});

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

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(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,但服务端不会有任何操作
  • 与token简单对比
  • session要求服务端存储信息,并且根据id能够检索,而token不需要,因为信息就在token中,这样实现就实现了服务器端的无状态化,在大规模的系统中,对每个请求都检索会话信息的可能是一个复杂和耗时的过程,但另外一方面服务器要通过token来解析用户身份也需要定义好相应的协议,比如jwt.
  • session一般通过cookie来交互,而token方式更加灵活,可以是cookie,也可以是 header,也可以放在请求的内容中。不使用cookie可以带来跨域上的便利性。
  • token的生成方式更加多样化,可以由第三方模块来提供。
  • token若被盗用,服务端无法感知,cookie信息存储在用户自己电脑中,被盗用风险略小。
JWT(JSON WEB TOKEN)原理解析
  1. Bearer Token包含三个组成部分:令牌头、payload、哈希eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0Ij oxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9A JGCHG5fT_WnEaf72EA
    第三个参数 ??? base64 可逆
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0Ij oxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U
  2. 签名:默认使用base64对payload编码,使用hs256算法对令牌头、payload和密钥进行签名生成 哈希
  3. 验证:默认使用hs256算法对hs256算法对令牌中数据签名并将结果和令牌中哈希比对

OAuth(开放授权)

概念:三方登入主要基本于OAuth 2.0 OAuth协议为用户资源的授权提供了一个案例的,开放而又简易的标准,与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的账号信息,如用户名与密码,即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的

OAUTH的登录流程

<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">
    <button @click='oauth()'>Login with Github</button>
    <div v-if="userInfo">
      Hello {{userInfo.name}}
      <img :src="userInfo.avatar_url" />
    </div>
  </div>
  <script>

  </script>
  <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: {
        logs: [],
        userInfo: null
      },
      methods: {
        async oauth() {
          window.open('/auth/github/login', '_blank')
          const intervalId = setInterval(() => {
            console.log("等待认证中..");
            if (window.localStorage.getItem("authSuccess")) {
              clearInterval(intervalId);
              window.localStorage.removeItem("authSuccess");
              this.getUser()
            }
          }, 500);
        },
        async getUser() {
          const res = await axios.get("/auth/github/userinfo");
          console.log('res:',res.data)
          this.userInfo = res.data
        }
      }
    });
  </script>
</body>
</html>
const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const app = new Koa();
const axios = require('axios')
const querystring = require('querystring')
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
const accessTokens = {}

const secret = "it's a secret";
app.use(static(__dirname + '/'));
const config = {
    client_id: '73a4f730f2e8cf7d5fcf',
    client_secret: '74bde1aec977bd93ac4eb8f7ab63352dbe03ce48',

}

router.get('/auth/github/login', async (ctx) => {
    var dataStr = (new Date()).valueOf();
    //重定向到认证接口,并配置参数
    var path = `https://github.com/login/oauth/authorize?${querystring.stringify({ client_id: config.client_id })}`;

    //转发到授权服务器
    ctx.redirect(path);
})

router.get('/auth/github/callback', async (ctx) => {
    console.log('callback..')
    const code = ctx.query.code;
    const params = {
        client_id: config.client_id,
        client_secret: config.client_secret,
        code: code
    }
    let res = await axios.post('https://github.com/login/oauth/access_token', params)
    const access_token = querystring.parse(res.data).access_token
    const uid = Math.random() * 99999
    accessTokens[uid] = access_token

    const token = jwt.sign(
        {
            data: uid,
            // 设置 token 过期时间,一小时后,秒为单位
            exp: Math.floor(Date.now() / 1000) + 60 * 60
        },
        secret
    )
    ctx.response.type = 'html';
    console.log('token:', token)
    ctx.response.body = ` <script>window.localStorage.setItem("authSuccess","true");window.localStorage.setItem("token","${token}");window.close();</script>`;
})

router.get('/auth/github/userinfo', jwtAuth({
    secret
}), async (ctx) => {
    // 验证通过,state.user
    console.log('jwt playload:', ctx.state.user)
    const access_token = accessTokens[ctx.state.user.data]
    res = await axios.get('https://api.github.com/user?access_token=' + access_token)
    console.log('userAccess:', res.data)
    ctx.body = res.data
})

app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
app.listen(7001);
单点登录

...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,540评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,640评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,643评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,672评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,525评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,387评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,809评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,450评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,743评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,787评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,560评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,406评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,824评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,048评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,335评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,784评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,997评论 2 337

推荐阅读更多精彩内容