11.koa鉴权

1,228 阅读7分钟

前言

前后端未分离以前,⻚⾯都是通过后台来渲染的,能不能访问到⻚⾯直接由后台逻辑判断。前后端分离以后,⻚⾯的元素由⻚⾯本身来控制,所以⻚⾯间的路由是由前端来控制了。当然,仅有前端做权限控制是远远不够的,后台还需要对每个接⼝做验证。

为什么前端做权限控制是不够的呢?因为前端的路由控制仅仅是视觉上的控制,前端可以隐藏某个⻚⾯或者某个按钮,但是发送请求的⽅式还是有很多,完全可以跳过操作⻚⾯来发送某个请求。所以就算前端的权限控制做的⾮常严密,后台依旧需要验证每个接⼝。

前端的权限控制主要有三种:路由控制(路由的跳转)、视图控制(按钮级别)和请求控制(请求拦截器)。前端做完权限控制,后台还是需要验证每⼀个接⼝,这就是鉴权。现在前后端配合鉴权的⽅式主要有以下⼏种:

  • session-cookie
  • Token验证(JWT)
  • OAuth(开放授权)

Session/Cookie

cookie

Http协议是⼀个⽆状态的协议,服务器不会知道到底是哪⼀台浏览器访问了它,因此需要⼀个标识⽤来让服务器区分不同的浏览器。cookie就是这个管理服务器与客户端之间状态的标识。

cookie的原理是,浏览器第⼀次向服务器发送请求时,服务器在response头部设置Set-Cookie字段,浏览器收到响应就会设置cookie并存储,在下⼀次该浏览器向服务器发送请求时,就会在request头部⾃动带上Cookie字段,服务器端收到该cookie⽤以区分不同的浏览器。当然,这个cookie与某个⽤户的对应关系应该在第⼀次访问时就存在服务器端,这时就需要session了。

session

session是会话的意思,浏览器第⼀次访问服务端,服务端就会创建⼀次会话,在会话中保存标识该浏览器的信息。它与 cookie的区别就是session是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端⽣成,为了弥补Http协议⽆状态的缺陷。

session-cookie认证

原理:

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

session的签名使用

安装

cnpm i koa-session -S 使用

//引入
const koaSession = require('koa-session');

// 模拟签名
// app.keys 签名的 cookie 密钥数组,用来对cookie进行签名
app.keys = ['session secret'];

//设置签名
const SESSION_CONFIG = {
    key: 'alex', //设置 cookie 的 key 名字
    maxAge: 86400, //设置有效期
    httpOnly: true, //仅服务器修改,防止客户端篡改签名
    signed: true, //签名cookie
}
// 参数:配置   需要添加配置的实例
app.use(koaSession(SESSION_CONFIG, app));
//测试 session-cookie
app.use(async ctx => {
    let n = ctx.session.count || 0;
    ctx.session.count = ++n;
    ctx.body = `第${n}次访问`;
})

将session的数据存入到redis

redis主要用于存储缓存性的数据.

安装

首先电脑上也要下载 redis.

cnpm i redis koa-redis -S 使用

//引入 redis
const redis = require('redis');
//导入仓库
const redisStore = require('koa-redis');

//redis创建客户端
const client = redis.createClient(6379, 'localhost');

//配置
//设置签名
const SESSION_CONFIG = {
    key: 'alex', //设置 cookie 的 key 名字
    maxAge: 86400, //设置有效期
    httpOnly: true, //仅服务器修改,防止客户端篡改签名
    signed: true, //签名cookie
    store: redisStore({
        ...client
    }), //配置缓存到redis
}
// 参数:配置   需要添加配置的实例
app.use(koaSession(SESSION_CONFIG, app));
//测试 session-cookie
app.use(async ctx => {
    let n = ctx.session.count || 0;
    ctx.session.count = ++n;
    ctx.body = `第${n}次访问`;
    //从redis中读取数据
    client.keys('*', (err, keys) => {
        console.log(keys);
        keys.forEach(key => {
            client.get(key, (err, val) => {
                console.log(val);
            })
        });
    })
})

session/cookie实现用户登录鉴权

前端通过cookie来存储用户的登录信息,后端通过session来判断用户是否登录.

前端

// 登录方法
async handleLogin() {
  let res = await axios.post('/user/login', {
    user: this.user,
    pwd: this.pwd
  });
  console.log(res);
},
// 退出方法
async handleLogout() {
  await axios.post('/user/logout');
},
// 获取用户信息
async getUser() {
  let res = await axios.get('/user/getUser');
  console.log(res);
}

user.js

const Router = require('koa-router');
//实例化并设置前缀
const router = new Router({
    prefix: '/user'
});

//登录
router.post('/login', async (ctx, next) => {
    // console.log(ctx.request.body);
    const {
        body
    } = ctx.request;
    console.log(body);
    //验证数据库,存储
    ctx.session.userInfo = body.user;
    ctx.body = {
        ok: 1,
        message: '登录成功'
    }
})
//获取用户,导航守卫,认证
//会先执行 require 导入的 auth 函数
router.get('/getUser', require('../middleware/auth'), async (ctx, next) => {
    ctx.body = {
        ok: 1,
        message: '获取数据成功',
        user: ctx.session.userInfo
    }
})
//退出
router.post('/logout', async (ctx, next) => {
    delete ctx.session.userInfo;
    ctx.body = {
        ok: 1,
        message: '退出成功'
    }
})

auth.js

若用户没有登录,就返回未登录信息,否则直接放行。

module.exports = async (ctx, next) => {
    if (!ctx.session.userInfo) {
        ctx.body = {
            ok: 0,
            message: '用户未登录'
        }
    } else {
        //放行
        await next();
    }
}

Token+JWT认证

资料

下载

npm i koa-jwt -S

npm i jsonwebtoken -S

登录认证

前端

async handleLogin() {
  const res = await axios.post('/user/login-token', {
    user: this.user,
    pwd: this.pwd
  })
  //存储到本地
  localStorage.setItem('token', res.data.token);
},
async handleLogout() {
  localStorage.removeItem('token');
},
async getUser() {
  await axios.get('/user/getUser-token')
}

user.js

const Router = require('koa-router');
const jwt = require('jsonwebtoken'); //生成令牌的模块
const jwtAuth = require('koa-jwt'); //认证令牌的模块
const secret = 'this is a secret'; //签名
//实例化并设置前缀
const router = new Router({
    prefix: '/user'
});

//token认证
router.post('/login-token', async (ctx, next) => {
    const {
        body
    } = ctx.request;
    console.log(body);
    const userInfo = body.user;
    //省略...登录操作
    ctx.body = {
        ok: 1,
        message: '登录成功',
        user: userInfo,
        //生成令牌
        token: jwt.sign({
            data: userInfo, //由于签名不是加密的,令牌不要存放敏感数据
            exp: Math.floor(Date.now() / 1000) + 60 * 60, //过期时间1小时
        }, secret /*签名*/ )
    }
})
//获取用户信息
router.get('/getUser-token', jwtAuth( /*认证*/ {
    secret
}), async (ctx, next) => {
    ctx.body = {
        ok: 1,
        message: '数据获取成功',
        userInfo: ctx.state.user.data, //在生成令牌的同时将数据保存到了state中
    }
})

如果需要对更多的路由进行认证,可以将 jwtAuth({secret}) 提升到更高层级(server.js),同时,也可以通过 jwtAuth({secret}).unless([ ]) 将不需要验证的路由写入数组中

OAuth(开放授权)

有的应⽤会提供第三⽅应⽤登录,⽐如掘⾦ web 客户端提供了 微信、QQ账号登录,我们可以不⽤注册掘⾦账号,⽽可以⽤已有的微信账号登录掘⾦。看看⽤微信登录掘⾦的过程:

step1: 打开掘⾦,未登录状态,点击登录,掘⾦给我们弹出⼀个登录框,上⾯有微信、QQ登录选项,我们选择微信登录;
step2: 之后掘⾦会将我们重定向到微信的登录⻚⾯,这个⻚⾯给出⼀个⼆维码供我们扫描,扫描之后;
step3: 我们打开微信,扫描微信给的⼆维码之后,微信询问我们是否同意掘⾦使⽤我们的微信账号信息,我们点击同意;
step4: 掘⾦刚才重定向到微信的⼆维码⻚⾯,现在我们同意掘⾦使⽤我们的微信账号信息之后,⼜重定向回掘⾦的⻚⾯,同时我们可以看到现在掘⾦的⻚⾯上显示我们已经处于登录状态,所以我们已经完成了⽤微信登录掘⾦的过程

这个过程⽐我们注册掘⾦后才能登录要快捷多了。这归功于OAuth2.0 ,它允许客户端应⽤(掘⾦)可以访问我们的资源服务器(微信),我们就是资源的拥有者,这需要我们允许客户端(掘⾦)能够通过认证服务器(在这⾥指微信,认证服务器和资源服务器可以分开也可以是部署在同⼀个服务上)的认证。OAuth 2.0 提供了4种⻆⾊,资源服务器、资源的拥有者、客户端应⽤和认证服务器,它们之间的交流实现了OAuth 2.0 整个认证授权的过程。

原理图

  • 第一次模拟发起浏览器请求是获取令牌
  • 第二次模拟发起浏览器请求是携带令牌获取数据

使用github实现OAuth登录授权

首先需要去github上登记一个应用

<a href="/user/login-github">github登录</a>

user.js

安装 axios

cnpm i axios -S

const Router = require('koa-router');
const axios = require('axios');
const querystring = require('querystring'); //引入内置的库
//实例化并设置前缀
const router = new Router({
    prefix: '/user'
});

// OAuth 授权登录
const config = {
    // id 和 密钥
    client_id: 'ca9133d5deda0d821d4d',
    client_secret: '4ede50d2e312f8526c79e83640a386ad745a23ad'
}
router.get('/login-github', async (ctx, next) => {
    // 重定向到认证接⼝,并配置参数 
    const path = `https://github.com/login/oauth/authorize?client_id=${config.client_id}`;
    // 转发到授权服务器 
    ctx.redirect(path);
})

router.get('/oauth/github/callback', async ctx => { //这是一个授权回调,用于获取授权码 code
    const code = ctx.query.code; // GitHub 回调传回 code 授权码
    // 带着 授权码code、client_id、client_secret 向 GitHub 认证服务器请求 token
    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;
    // 带着 token 从 GitHub 获取用户信息
    // res = await axios.get('https://api.github.com/user?access_token=' + access_token);
    res = await axios({
        method: 'get',
        url: `https://api.github.com/user`,
        headers: {
            accept: 'application/json',
            Authorization: `token ${access_token}`
        }
    });
    console.log('userAccess', res.data);
    ctx.redirect('/hello.html');
})