鉴权

880 阅读5分钟

目录

cookie-session模式

  1. session源码实现

cookie是什么

  • 服务器通过Set-Cookie 响应头设置Cookie
  • 浏览器得到Cookie后,每次请求会自动带上Cookie
  • 服务器读取Cookie就知道登录用户的信息(req.header.cookie)
/**
 * cookie测试
 * 内存cookie:不设置过期时间默认是存在浏览器的内存中,浏览器关闭后自动清除
 * 硬盘cookie:是指在你设置了cookie的Expires属性(即过期时间),此时cookie将保存到你的硬盘上,过期时间到了会自动清除或者手动清除
 */
const http = require('http')

http.createServer((req, res) => {
    if (req.url === '/favicon.ico') return

    console.log(req.headers.cookie)

    // cookie设置过期时间后,浏览器会在30秒后自动清除
    // let exp = new Date();
    // exp.setTime(exp.getTime() + 30 * 1000);//过期时间30秒
    // res.setHeader('Set-Cookie', 'cookie1=a;expires=' + exp.toGMTString())

    //不设置cookie过期时间 浏览器关闭后自动清除
    res.setHeader('Set-Cookie', 'cookie1=a')
    res.end('hello')
})
    .listen(3000)

什么是session, cookie和session的区别是什么?

/**
 * session 测试
 * session是依赖Cookie实现的。session是服务器端对象
 * 自己的见解:session就是将用户的敏感信息存储在服务器中,通过将对应的sid保存在cookie中
 * 用户在发送请求时携带带有sid的cookie,服务器拿到sid,就可以得到相应的用户信息
 * session的局限性:在数据共享,跨域,集群服务器上难以施展
 * 解决方案:
 * 1. session 数据持久化,写入数据库或别的持久层。如Redis键值对数据库,这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
 * 2.JWT 一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。
 * 
 */

const http = require('http')

//新建服务器对象
const session = {}

http.createServer((req, res) => {
    if (req.url === '/favicon.ico') return

    console.log(req.headers.cookie)
    const sessionKey = 'sid'
    const cookie = req.headers.cookie

    // 如果有session
    if (cookie && cookie.indexOf(`${sessionKey}`) > -1) {
        res.end('Come Back!')
        //从cookie中提取sid,然后通过sid查询对应的用户数据
        const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`)
        const sid = pattern.exec(cookie)[1]
        console.log('session:', sid, session, session[sid])
    } else {
        const sid = (Math.random() * 9999999999).toFixed()
        res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
        //在服务器对象session中,根据索引sid来填充数据
        session[sid] = {
            name: '柳絮才媛'
        }
        res.end('第一次')
    }
    res.end('hello')
})
    .listen(3000)
  1. koa session
/**
 * 首次访问后,浏览器会收到一个cookie键值对
 * lxcy:sess =  eyJ2aWV3IjoyLCJfZXhwaXJlIjoxNTcwNjkyNzAzNTA2LCJfbWF4QWdlIjo4NjQwMDAwMH0=
 * 再次发送请求会携带此cookie值
 */

const Koa = require('koa')
const app = new Koa()
const session = require('koa-session')

// 加密私钥
app.keys = ['some secret key']

// session配置
const CONFIG = {
    key: 'lxcy:sess',
    maxAge: 86400000,
    httpOnly: true,
    signed: true
}

app.use(session(CONFIG, app))

app.use(ctx => {
    if(ctx.url == '/favicon.ico') return 

    //首次是一个空对象
    console.log(ctx.session)

    let n = ctx.session.view || 0
    ctx.session.view = ++n

    ctx.body = `第 ${n} 次 访问`
})

app.listen(3000)


  1. Redis 全局session
/**
 * redis键值对服务器
 * 通过set和get来存储数据
 * session做数据持久化可以使用Redis数据库,可以实现数据共享
 */

const redis = require('redis')
const client = redis.createClient(6379, 'locahost')

client.set('name', 'hello redis')

client.get('name',(err,val) => {
    console.log('redis get:', val)
})
/**
 * koa-redis测试
 * 首先要安装Redis数据库,采用docker安装
 */

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


//包装后,可以直接使用ES7语法
var wrapper = require('co-redis');
var client = wrapper(redisClient);

app.keys = ['some secret key']

app.use(session({
    key: 'kkb:sess',
    store: redisStore({ client })
}, app));


app.use(ctx => {
    //...
    //查看redis中存储的数据
    redisClient.keys('*', (err, keys) => {
        console.log(keys);
        keys.forEach(key => {
            redisClient.get(key, (err, val) => {
                console.log(val);
            })
        })
    })
});

app.listen(3000)

token JWT模式

  1. Json Web Token 原理

参考资料:

/**
 * JWT原理:
 * Bearer Token包含三个组成部分:令牌头、payload、哈希
 * token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWJjIiwicGFzc3dv
cmQiOiIxMTExMTEifSwiZXhwIjoxNTQ2OTQyMzk1LCJpYXQiOjE1NDY5Mzg3OTV9.VPBCQgLB7XPBq3RdHK9WQM
kPp3dw65JzEKm_LZZjP9Y
 * 1. 签名:默认使用base64对令牌头和payload进行编码,使用hs256算法对(令牌头、payload和密钥)进行签名生成哈希
 * 2. 验证:默认使用hs256算法对令牌中数据签名并将结果和令牌中哈希比对
 */ 

const jsonwebtoken = require('jsonwebtoken')

//密钥
const secret = '12345678'

//token中存放用户相关信息,最好不要存放敏感信息
const user = {
  username: 'zhongyuan',
  password: '12345'
}

//默认使用HMAC SHA256算法,简称HS256
const token = jsonwebtoken.sign({
  data: user,
  // 设置 token 过期时间
  exp: Math.floor(Date.now() / 1000) + (60 * 60), 
}, secret)

console.log('生成token:' + token)
console.log('解码:', jsonwebtoken.verify(token, secret))

  1. koa-jwt
/**
 * JWT鉴权
 * 通过对ctx.session的设置和删除来判断用户是否登录过
 * 服务器端操作
 */
const Koa = require('koa')
const app = new Koa()
const jwt = require('jsonwebtoken') //签名
const jwtAuth = require('koa-jwt') //验证
const bodyparser = require('koa-bodyparser')
const router = require('koa-router')()
const static = require('koa-static')

// 密钥
const secret = 'some secret'

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

//登录,将用户信息作为负载写入token,返回token数据
router.post('/users/login-token', ctx => {
    const {body} = ctx.request

    ctx.body = {
        code: 200,
        user:  body.username,
        //token签名
        token: jwt.sign({
            data: body.username,
            exp: Math.floor(Date.now() / 1000) + (60 * 60),
        },secret),
        message:'登录成功'
    }
})

//中间件验证token信息
router.get('/users/getUser-token',jwtAuth({secret}), ctx => {
    ctx.body= {
        code: 200,
        username: ctx.state.user
    }
})


app.use(router.routes())

app.listen(3000)

//前端核心代码index.html
<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: "zhongyuan",
                password: "123456",
                logs: []
            },
            methods: {
                login: async function () {
                    const res = await axios.post("/users/login-token", {
                        username: this.username,
                        password: this.password
                    });
                    localStorage.setItem("token", res.data.token);
                },
                logout: async function () {
                    localStorage.removeItem("token");
                },
                getUser: async function () {
                    await axios.get("/users/getUser-token");
                }
            }
        });
    </script>

  1. 优势

基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。

jwt的优点:

  1. 可扩展性好

应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。

  1. 无状态

jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

缺点:

  1. 安全性
  2. 性能
  3. 一次性

具体看参考资料第3个


扩展

  1. oauth2模式(第三方登录)

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

所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权

授权流程:

  • A 网站让用户跳转到 GitHub。 GitHub 要求用户登录,然后询问"A 网站要求获得 xx 权限,你是否同意?"
  • 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码。
  • A 网站使用授权码,向 GitHub 请求令牌。
  • GitHub 返回令牌.
  • A 网站使用令牌,向 GitHub 请求用户数据

准备工作:

要求OAuth授权,先到对方网站登记 (GitHub登记地址) 登记应用网站的地址和授权回调地址 成功后会返回client_id和client_secret,用来之后登录GitHub获取授权码和令牌

//index.js
/**
 * github第三方登录测试
 * 1. 先到GitHub进行登记获取client_id和client_secret
 * 2. 使用client_id向GitHub获取授权码
 * 3. github 根据登记时配置的回调地址,带着授权码重定向到该地址
 * 4. 使用授权码、client_id和client_secret向GitHub获取授权令牌
 * 5. 使用授权令牌获取用户信息
 */

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')

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

//先到GitHub进行登记获取client_id和client_secret
const config = {
    client_id: '登记获取',
    client_secret: '登记获取'
}
//使用client_id向GitHub获取授权码
router.get('/github/login', async (ctx) => {
    var dataStr = (new Date()).valueOf();
    //重定向到认证接口,并配置参数
    var path = "https://github.com/login/oauth/authorize";
    path += '?client_id=' + config.client_id;
    //转发到授权服务器
    ctx.redirect(path);
})

//github 根据登记时配置的回调地址(http://localhost:3000/github/oauth/callback),带着授权码重定向到该地址
router.get('/github/oauth/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
    }
    //使用授权码、client_id和client_secret向GitHub获取授权令牌
    let res = await axios.post('https://github.com/login/oauth/access_token',
        params)
    const access_token = querystring.parse(res.data).access_token
    console.log(access_token)
    //使用授权令牌获取用户信息
    res = await axios.get('https://api.github.com/user?access_token=' +
        access_token)
    console.log('userAccess:', res.data)
    ctx.body = `
    <h1>Hello ${res.data.login}</h1>
    <img src="${res.data.avatar_url}" alt=""/>
    `
})
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">
        <a href='/github/login'>login with github</a>
    </div>
</body>
</html>

学习资料:

  1. oss(单点登录)

原理: 单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统

多个系统应用中A1、A2、A3...(负责业务和逻辑)

sso系统:只做统一的登录和授权验证系统

推荐文章:

单点登录(SSO)看这一篇就够了

单点登录实例源码

看过源码后自己的见解:

  • SSO系统做统一登录和授权验证,访问SSO系统先判断登录态,已登录则直接重定向应用系统带上ST,未登录则定向到登录页,登录后记录登录态(cookie)
  • 首次访问应用系统A,未登录,则跳转SSO登录页,登录成功,SSO记录登录态(设置cookie),返回ST,系统A再次发送ST到SSO进行验证,通过后可获取用户信息,系统A记录自己的登录态(session)
  • 然后访问系统B,判断B是否有登录态(session),没有,跳转到SSO登录,此时SSO有登录态(cookie有值),则直接将token以参数的形式重定向到系统B,B再用ST向SSO发送验证请求,验证通过后,返回用户信息,记录B登录态(session)
  • 再次登录A或者B时,查看登录态(session),则可以直接进入系统

==提问:== 为什么SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态

答:试想如果我在SSO没有登录,而是直接在浏览器中敲入应用的回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?

而是先重定向到当前系统,然后在发送授权验证请求,通过后得到相应的数据,这样安全性更高,没办法伪造请求地址


  • 文章 源码
  • 微信号:yuan_zi_xxx