美文网首页
nodejs鉴权

nodejs鉴权

作者: 巧克力_404 | 来源:发表于2021-05-08 15:45 被阅读0次

三种常见鉴权方式

  • 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);
单点登录

...

相关文章

  • nodejs鉴权

    三种常见鉴权方式 Session/CookieToken OAuth SSO session-cookie方式 由...

  • 谈谈鉴权与授权

    目录 鉴权场景实现 授权场景实现 鉴权 鉴权(authentication): 你是谁 场景 实现 关于鉴权的示例...

  • 常见的鉴权方式,你真的不想知道吗

    主要内容 鉴权的作用 几种常见的鉴权 各个鉴权的适用场景 一、什么是鉴权 鉴权是指验证用户是否有权利访问系统的行为...

  • 云调用,小程序鉴权-方案

    目录:一、无处不在的鉴权 现实生活中的身份鉴权方法 简单的密码鉴权体系二、鉴权优化 频繁的鉴权场景下的优化方案 第...

  • jwt实现token鉴权(nodejs koa)

    为什么需要token? 在后台管理系统中,我们通常使用cookie-session的方式用于鉴权,如何通过cook...

  • JWT鉴权 Session鉴权

    JWT鉴权:image.png session鉴权:image.png

  • 鉴权与Web安全

    什么是鉴权 鉴权(authentication)是指验证用户是否拥有访问系统的权利。传统的鉴权是通过密码来验证的。...

  • 04-15动态路由的实现与优化

    Vue中后台鉴权的另一种思路 - 动态路由的实现与优化 鉴权-前端路由 VS 鉴权-动态路由 前端路由鉴权相信只要...

  • 鉴权和权限管理的探索

    传统鉴权方案是通过Session ID完成的,现在一般使用Token鉴权。 1.认证与鉴权 分布式session ...

  • 鉴权方式整理

    最近在做 aPaaS 相关的项目,用到各种用户鉴权的方式。所以对鉴权方式做个整理。 常见的鉴权方式: HTTPBa...

网友评论

      本文标题:nodejs鉴权

      本文链接:https://www.haomeiwen.com/subject/ieoydltx.html