1、session认证原理


session.drawio.png

流程大致如下:

  • 进入某个平台的页面后会发送一些请求,第一次请求时没有携带cookie,自动条状到SSO进行登录
  • 登陆后,SSO会生成sessionID并保存到cookie中,然后重定向到应用平台
  • 接着发送的请求会携带cookie,如果session过期,那么跳转到SSO再一次登录

2、window下redis安装与使用


session会话保存到redis集群中,实现服务器共享session。

2.1、安装redis

  • 使用wget或者直接到github下载redis压缩包,将压缩包解压到任何文件夹中,比如D:\redis
  • 设置密码,打开redis.windows.conf文件,添加requirepass ****,如下图:
    image2020-7-14_18-6-55.png
  • redis-server.exe使用本地配置启动redis

    cd D:\redis
    
    redis-server.exe redis.windows.conf
  • 运行redis-cli.exe以连接到您的redis实例

    cd D:\redis
    
    redis-cli.exe
  • 开始使用redis

PS:如果报了(error) NOAUTH Authentication required.是因为设置了认证密码,需要输入密码(就是上面设置的密码)进行认证登入

2.2、使用图形化工具连接redis

Redis React是一个简单易用的用户界面,用于浏览Redis服务器中的数据,该数据由React桌面模板构建,可在Windows,OSX,Linux等多种平台上使用,也可以部署为自托管控制台或ASP.NET Web应用程序。

Redis React充分利用了基于Web的UI的导航和深层链接优势,React框架的生产力和响应能力 以及本机桌面应用程序提供的丰富的本机体验和OS集成。

3、模拟redis集群

在本地启动redis就可以轻松的模拟redis集群,所有其他服务器直接连接redis就可以了

本文后台使用nodeJs实现,生成session和操作redis依赖express-sessionconnect-redisredis三个库。

express-session:中间件,用于在后端生成sessionID和cookie,会在request流对象中生成一个Session对象,用于操作session。
connect-redis:这是一个关于session的持久化插件, 配合express-session使用。此模块基于redis,将session相关信息持久化(意思就是将session存入redis)。
redis:用于连接和操作redis数据库。

使用方式大致如下:

const express = require("express")
const session = require('express-session')
const RedisStrore = require('connect-redis')(session)
const redis = require('redis')


const config={
  "cookie" : {
    "path": "/",
    "maxAge" : 1800000,
    "httpOnly": true
  },
  "sessionStore" : {
    "host": "127.0.0.1", // redis主机
    "port": "6379", // redis默认端口号
    "pass": "****",
    "auth_pass": "****", // 设置了密码时需要该字段
  }
}

const app = express()
const client = redis.createClient(6379, '127.0.0.1', config.sessionStore) // 连接redis,创建实例

app.use(session({
  name : "sessionId", // sessionID的属性名
  secret : 'Asecret123-', // 生成sessionID的密钥
  resave : false,
  rolling: false, //在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false)
  saveUninitialized : false, // 强制将未初始化的 session 存储。当新建了一个 session 且未设定属性或值时,它就处于未初始化状态。在设定一个 cookie 前,这对于登陆验证,减轻服务端存储压力,权限控制是有帮助的。(默 认:true)。建议手动添加。
  cookie: config.cookie,
  store: new RedisStrore({ client })
}));

使用后request流对象会新增sessionID字段和session对象

image.png

3、应用平台实现


前端部分:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
</head>
<body>
  <div id="app">home</div>
  <button onclick="logout()">退出</button>
</body>
</html>
<script>
  $.ajax({
    url: "http://127.0.0.1:1001/getData",
    method: 'GET',
    dataType: 'json',
    xhrFields: {
      withCredentials: true //允许携带Cookie
    },
    success: function(res) {
      console.log(res)
      if (res.code === 401) {
        window.location.href = "http://127.0.0.1:2000/?redirect_url=" + encodeURIComponent(window.location.href)
      } else if (res.code === 200) {
        $("#app").text(res.data.msg)
      }
    }
  })

  function logout() {
    $.ajax({
      url: "http://127.0.0.1:1001/logout",
      method: 'GET',
      dataType: 'json',
      xhrFields: {
        withCredentials: true //允许携带Cookie
      },
      success: function(res) {
        if (res.code === 200) {
          // 不能再这里操作document.cookie来删除sessionId,因为设置了httpOnly
          window.location.reload()
        }
      }
    })
  }
</script>

后端部分:

// 拦截器
app.all('*', function(req, res, next) {
  if (req.cookies.sessionId) {
    var sessionId = req.cookies.sessionId.split('.')[0].replace("s:", "sess:")
    console.log('sessionId', sessionId)
    client.get(sessionId, function(err, reply) {
      console.log('reply', reply)
      if (reply) {
        next();
      }
    })
  } else {
    console.log("=============重定向到SSO=============")
    res.json({
      code: 401
    })
  }
})

app.get('/getData', (req, res) => {
  res.json({
    code: 200,
    data: {
      msg: 'hello, welcome you!'
    }
  })
})

app.get('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) throw new ErrorEvent("注销失败")
    res.clearCookie("sessionId").json({
      code: 200
    })
  })
})

初始进入页面发送http://127.0.0.1:1001/getData请求,如果没有携带cookie,后台拦截器就会拦截,返回401,前台判断是401就重定向到SSO

4、SSO实现


前端部分:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>login</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
</head>
<body>
    <div>
      <label>账户:</label>
      <input type="text" class="account" name="account" value="">
    </div>
    <div>
      <label>密码:</label>
      <input type="text" class="password" name="password" value="">
    </div>
    <div>
      <input type="button" onclick="submit()" value="登录">
      <input type="reset" value="重置">
    </div>
</body>
</html>
<script>
  // 获取url参数
  function getQueryVariable(variable) {
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0;i<vars.length;i++) {
      var pair = vars[i].split("=");
      if(pair[0] == variable){return pair[1];}
    }
    return(false);
  }

  function submit() {
    var account = $(".account").val().trim()
    var password = $(".password").val().trim()
    if (!account || !password) {
      alert("账户密码不能为空")
    }
    $.ajax({
      url: 'http://127.0.0.1:2001/login',
      method: 'POST',
      data: { account, password },
      dataType: 'json',
      contentType: 'application/x-www-form-urlencoded',
      xhrFields: {
        withCredentials: true //允许携带Cookie
      },
      success: function(res) {
        if (res.code === 200) {
          setTimeout(() => {
            window.location.href = decodeURIComponent(getQueryVariable("redirect_url"))
          }, 1000);
        }
      } 
    })
  }
</script>

后台部分:

app.post('/login', (req, res) => {
  var account = req.body.account
  var pwd = req.body.password
  if (!account || !pwd) {
    console.log("账户密码不能为空")
    return
  }
  nosql.one().make(builder => {
    builder.where('account', '=', account);
    builder.where('password', '=', pwd);

    builder.callback((err, userInfo) => {
      if (userInfo) {
        req.session.regenerate(err => {
          if (err) {
            console.log('生成sessionID失败: ' + err)
          } else {
            console.log(req)
            req.session.userInfo = userInfo
            req.session.save()
            // res.cookie("sessionId", req.sessionID, req.session.cookie)
            res.json({
              code: 200
            })
          }
        })
      } else {
        console.log("该账户不存在")
      }
    })
  })
})

这里数据库使用的是nosql文档数据库,依赖nosql模块。登录成功后regenerate方法会生成session和cookie,然后将session保存到redis中

PS:存在cookie中的sessionID和存在redis中的sessionID不一样,这是因为express-session底层实现的原因,有兴趣的同学可以深入一下

项目地址:https://github.com/Revelation...

5、参考

https://segmentfault.com/a/11...
https://github.com/ServiceStackApps/RedisReact#download

https://github.com/ServiceStack/redis-windows


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。