初始化项目
# 初始化package.json
npm init
# 安装koa2
npm install koa
hello world 代码
const Koa = require('koa'); //引入Koa框架
const app = new Koa();
app.use( async ctx => {
ctx.body = 'Hello Koa';
})
// 监听3000端口
app.listen(3000,() => {
console.log('Server running at port 3000!');
})
node app.js // 启动服务
async/await的使用
function getSyncTime() {
return new Promise((resolve, reject) => {
try {
let startTime = new Date().getTime()
setTimeout(() => {
let endTime = new Date().getTime()
let data = endTime - startTime
resolve( data )
}, 500)
} catch ( err ) {
reject( err )
}
})
}
async function getSyncData() {
let time = await getSyncTime()
let data = `endTime - startTime = ${time}`
return data
}
async function getData() {
let data = await getSyncData()
console.log( data )
}
getData()
从上述例子可以看出 async/await 的特点:
- 可以让异步逻辑用同步写法实现
- 最底层的await返回需要是Promise对象
- 可以通过多层 async function 的同步写法代替传统的callback嵌套
koa-router中间件
安装koa-router中间件
npm i koa-router --save
koa-router的使用
const Koa = require('koa'); //引入Koa框架
const Router = require('koa-router'); //引入koa-router 中间件
// 实例化 app
const app = new Koa();
// 路由
const router = new Router();
router.get('/', async (ctx) => {
ctx.body = 'Router 请求';
});
// 配置路由
app.use(router.routes()).use(router.allowedMethods());
// 监听3000端口
app.listen(3000,() => {
console.log('Server running at port 3000!');
})
请求数据获取
GTET请求数据获取
const Koa = require('koa'); //引入Koa框架
const Router = require('koa-router'); //引入koa-router 中间件
// 实例化 app
const app = new Koa();
// 路由
const router = new Router();
router.get('/user', async (ctx) => {
let url = ctx.url
// 从上下文的request对象中获取
let request = ctx.request
let req_query = request.query
let req_querystring = request.querystring
// 从上下文中直接获取
let ctx_query = ctx.query
let ctx_querystring = ctx.querystring
ctx.body = {
url,
req_query,
req_querystring,
ctx_query,
ctx_querystring
}
});
// 配置路由
app.use(router.routes()).use(router.allowedMethods());
// 监听3000端口
app.listen(3000,() => {
console.log('Server running at port 3000!');
})
POST请求数据获取
koa-bodyparser中间件
npm install koa-body --save
const Koa = require('koa'); //引入Koa框架
const Router = require('koa-router'); //引入koa-router 中间件
const bodyParser = require('koa-bodyparser');
// 实例化 app
const app = new Koa();
// 使用ctx.body解析中间件
app.use(bodyParser());
/* 支持json,form,text类型
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
*/
// 路由
const router = new Router();
router.post('/login', async ctx => {
console.log(ctx.request.body);
})
// 配置路由
app.use(router.routes()).use(router.allowedMethods());
// 监听3000端口
app.listen(3000,() => {
console.log('Server running at port 3000!');
})
静态资源加载
koa-static中间件使用
const Koa = require('koa');
const path = require('path');
const static = require('koa-static');
const app = new Koa();
// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'
app.use(static(
path.join( __dirname, staticPath)
))
app.use( async ( ctx ) => {
ctx.body = 'hello world'
})
app.listen(3000);
koa-static-router中间件, 实现多个&&多层路由加载静态资源
npm install koa-static-router
简单配置
app.use(static('public')) //默认配置: {dir:public route:'/public'}
单个路由
const static = require('koa-static-router');
app.use(static({
dir, //静态资源目录对于相对入口文件index.js的路径
router //路由命名
}))
多个路由
访问 localhost:3000/public/image/dir/1.png
访问 localhost:3000/static/image/dir/2.png
const Koa = require('koa')
const app = new Koa()
const static = require('koa-static-router');
// 单个路由
// app.use(static({
// dir:'public',
// router:'/static/' //路由长度 =1
// }))
//多个路由
app.use(static([
{
dir:'public', //静态资源目录对于相对入口文件index.js的路径
router:'/public/image/' //路由命名 路由长度 =2
},{
dir:'static', //静态资源目录对于相对入口文件index.js的路径
router:'/static/image/' //路由命名 路由长度 =2
}
]))
app.use( async ( ctx ) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('build success')
})
cookie/session
koa2使用cookie
koa提供了从上下文直接读取、写入cookie的方法
- ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
- ctx.cookies.set(name, value, [options]) 在上下文中写入cookie
koa2 中操作的cookies是使用了npm的cookies模块,源码在github.com/pillarjs/co…
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
if ( ctx.url === '/index' ) {
ctx.cookies.set(
'cid',
'hello world',
{
domain: 'localhost', // 写cookie所在的域名
path: '/index', // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date('2017-02-15'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite: false // 是否允许重写
}
)
ctx.body = 'cookie is ok'
} else {
ctx.body = 'hello world'
}
})
app.listen(3000, () => {
console.log('[demo] cookie is starting at port 3000')
})
koa2实现session
koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有一下几种
- 如果session数据量很小,可以直接存在内存中
- 如果session数据量很大,则需要存储介质存放session数据
koa-session的使用
npm install koa-session --save
const Koa = require('koa')
const app = new Koa()
const Koa_Session = require('koa-session');
// 这个是配合signed属性的签名key
const session_signed_key = ["some secret hurr"];
// 配置
const session_config = {
key: 'koa:sess', /** cookie的key。 (默认是 koa:sess) */
maxAge: 10*60*1000, /** session 过期时间,以毫秒ms为单位计算 。*/
autoCommit: true, /** 自动提交到响应头。(默认是 true) */
overwrite: true, /** 是否允许重写 。(默认是 true) */
httpOnly: true, /** 是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。 (默认 true) */
signed: true, /** 是否签名。(默认是 true) */
rolling: true, /** 是否每次响应时刷新Session的有效期。(默认是 false) */
renew: false, /** 是否在Session快过期时刷新Session的有效期。(默认是 false) */
};
// 实例化
const session = Koa_Session(session_config, app)
app.keys = session_signed_key;
// 使用中间件,注意有先后顺序
app.use(session);
app.use( async ( ctx ) => {
let n = ctx.session.views || 0;
ctx.session.views = ++n;
ctx.session.name = 'chen';
ctx.body = n + ' views';
})
app.listen(3000, () => {
console.log('[demo] cookie is starting at port 3000')
})
使用ejs模板
npm i koa-ejs --save
/**
* 项目入口文件
*/
const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
const Router = require('koa-router');
const router = new Router();
const render = require('koa-ejs');
const path = require('path');
app.use(bodyParser());
// 初始化ejs,设置后缀为html,文件目录为`views`
render(app, {
root: path.join(__dirname, 'views'),
layout: false,
viewExt: 'html',
cache: false,
debug: false
});
// 渲染首页
router.get('/',async (ctx,next)=>{
await ctx.render('index',{
title: '我是首页',
body: '我是内容啊'
});
})
app.use(router.routes());
app.use(router.allowedMethods());
// 监听3000端口
app.listen(3000);
连接mysql数据库
安装Sequelize
npm install mysql2 --save
npm install sequelize --save
使用 Sequelize 初始化连接池
创建数据表模型
数据表控制器
数据路由表
解决跨域问题
npm i koa2-cors --save
var koa = require('koa');
var app = new koa();
var router = require('koa-router')();
const cors = require('koa2-cors');
app.use(cors({
origin: '*', // 允许来自所有域名请求
/* origin: function (ctx) {
if (ctx.url === '/test') {
return "*"; // 允许来自所有域名请求
}
return 'http://localhost:8080'; / 这样就能只允许 http://localhost:8080 这个域名的请求了
},*/
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
router.post('/', async function (ctx) {
ctx.body = '恭喜 __小简__ 你成功登陆了'
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
实现文件上传
使用koa-multer 中间件
npm install koa-multer --save
const multer = require('koa-multer');//加载koa-multer模块
// 上传 图片
var storage = multer.diskStorage({
//文件保存路径
destination: function(req, file, cb) {
cb(null, 'public/uploads/')
},
//修改文件名称
filename: function(req, file, cb) {
var fileFormat = (file.originalname).split(".");
cb(null, Date.now() + "." + fileFormat[fileFormat.length - 1]);
}
})
//加载配置
var upload = multer({
storage: storage
});
router.post('/upload', upload.single('file'), async(ctx, next) => {
ctx.body = {
filename: ctx.req.file.filename //返回文件名
}
})
使用 koa-body 代替 koa-bodyparser 和 koa-multer
npm i koa-body -S
const koaBody = require('koa-body');
const Router = require('koa-router');
const app = new koa();
app.use(koaBody({
multipart:true, // 支持文件上传
encoding:'gzip',
formidable:{
uploadDir:path.join(__dirname,'public/upload/'), // 设置文件上传目录
keepExtensions: true, // 保持文件的后缀
maxFieldsSize:2 * 1024 * 1024, // 文件上传大小
onFileBegin:(name,file) => { // 文件上传前的设置
// console.log(`name: ${name}`);
// console.log(file);
},
}
}));
const router = new Router();
router.post('/',async (ctx)=>{
console.log(ctx.request.files); //上传的文件信息
console.log(ctx.request.body); //上传的所有信息
ctx.body = JSON.stringify(ctx.request.files);
});
koa-body 的基本参数
| 参数名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| patchNode | 将请求体打到原生 node.js 的ctx.req中 | Boolean | false |
| patchKoa | 将请求体打到 koa 的 ctx.request 中 | Boolean | true |
| jsonLimit | JSON 数据体的大小限制 | String / Integer | 1mb |
| formLimit | 限制表单请求体的大小 | String / Integer | 56kb |
| textLimit | 限制 text body 的大小 | String / Integer | 56kb |
| encoding | 表单的默认编码 | String | utf-8 |
| multipart | 是否支持 multipart-formdate 的表单 | Boolean | false |
| urlencoded | 是否支持 urlencoded 的表单 | Boolean | true |
| text | 是否解析 text/plain 的表单 | Boolean | true |
| json | 是否解析 json 请求体 | Boolean | true |
| jsonStrict | 是否使用 json 严格模式,true 会只处理数组和对象 | Boolean | true |
| formidable | 配置更多的关于 multipart 的选项 | Object | {} |
| onError | 错误处理 | Function | function(){} |
| stict | 严格模式,启用后不会解析 GET, HEAD, DELETE 请求 | Boolean | true |
formidable 的相关配置参数
| 参数名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| maxFields | 限制字段的数量 | Integer | 1000 |
| maxFieldsSize | 限制字段的最大大小 | Integer | 2 * 1024 * 1024 |
| uploadDir | 文件上传的文件夹 | String | os.tmpDir() |
| keepExtensions | 保留原来的文件后缀 | Boolean | false |
| hash | 如果要计算文件的 hash,则可以选择 md5/sha1 | String | false |
| multipart | 是否支持多文件上传 | Boolean | true |
| onFileBegin | 文件上传前的一些设置操作 | Function | function(name,file){} |
图片上传成功后,获取图片
const fs = require('fs')
const app = new Koa()
const router = new Router()
const serve = require('koa-static')
const koaBody = require('koa-body')
app
.use(serve(__dirname + '/files')) // files文件夹用于保存上传的文件,也是静态资源地址
.use(router.routes())
// 前端使用formData方式组装数据
router.post('/api/upload-files', koaBody({ jsonLimit: '2mb', multipart: true }), async (ctx) => {
const data = ctx.request.body.files.data;
const savePath = path.join(`./files`, data.name)
const reader = fs.createReadStream(data.path)
const writer = fs.createWriteStream(savePath)
const pro = new Promise( (resolve, reject) => {
var stream = reader.pipe(writer);
stream.on('finish', function () {
resolve(`http://当前服务器地址${data.name}`);
});
})
ctx.response.body = await pro
})
koa-send实现文件下载
const send = require('koa-send');
router.get('/download/:name', async (ctx) => {
const name = ctx.params.name;
const path = `upload/${name}`;
ctx.attachment(path);
await send(ctx, path);
});
koa中间件记录
koa的错误处理 koa-onerror
npm install koa-onerror --save
var onerror = require('Koa-onerror');
onerror(app);
koa日志 koa-logger
app.use(require('koa-logger'));
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
});
koa-json
美观的输出JSON response的Koa中间件 有两种使用方式: 一种是总是返回美化了的json数据:
const json = require('koa-json');
app.use(json());
另一种是默认不进行美化,但是当地址栏传入pretty参数的时候,则返回的结果是进行了美化的。
app.use(json({ pretty: false, param: 'pretty' }));
koa-jwt和jsonwebtoken进行token验证
koa-jwt 主要提供路有权限控制的功能,它会对需要限制的资源请求进行检查
token 默认被携带在Headers 中的名为Authorization的键值对中,koa-jwt也是在该位置获取token 的
app.use(jwt({ secret: 'shared-secret', key: 'jwtdata' }))
可以使用另外一ctx key来表示解码数据,然后就可以通过ctx.state.jwtdata代替ctx.state.user获得解码数据
secret的值可以使用函数代替,以此产生动态的加密秘钥koa-jwt依赖于jsonwebtoken和koa-unless两个库的
app.use(jwt({
secret:'chambers'
}).unless({path:[/^\/api/]}));
生成token
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel.js');
router.post('/login', async (ctx) => {
const data = ctx.request.body;
if(!data.name || !data.password){
return ctx.body = {
code: '000002',
data: null,
msg: '参数不合法'
}
}
const result = await userModel.findOne({
name: data.name,
password: data.password
})
if(result !== null){
const token = jwt.sign({
name: result.name,
_id: result._id
}, 'my_token', { expiresIn: '2h' });
return ctx.body = {
code: '000001',
data: token,
msg: '登录成功'
}
}else{
return ctx.body = {
code: '000002',
data: null,
msg: '用户名或密码错误'
}
}
});
module.exports = router;
在验证了用户名密码正确之后,调用 jsonwebtoken 的 sign() 方法来生成token,接收三个参数,第一个是载荷,用于编码后存储在 token 中的数据,也是验证 token 后可以拿到的数据;第二个是密钥,自己定义的,验证的时候也是要相同的密钥才能解码;第三个是options,可以设置 token 的过期时间。
jwt.decode(token); 可以解析出sign()中第一个参数。
获取token
前端请求头在Authorization中携带token,用于后台验证
每次请求都要获取 localStorage 中的 token,这样很麻烦,这里使用了 axios 的请求拦截器,对每次请求都进行了取 token 放到 headers 中的操作。
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
config.headers.common['Authorization'] = 'Bearer ' + token;
return config;
})
验证token
通过 koa-jwt 中间件来进行验证
const koa = require('koa');
const koajwt = require('koa-jwt');
const app = new koa();
// 错误处理
app.use((ctx, next) => {
return next().catch((err) => {
if(err.status === 401){
ctx.status = 401;
ctx.body = 'Protected resource, use Authorization header to get access\n';
}else{
throw err;
}
})
})
app.use(koajwt({
secret: 'my_token'
}).unless({
path: [/\/user\/login/]
}));
通过 app.use 来调用该中间件,并传入密钥 {secret: 'my_token'},unless 可以指定哪些 URL 不需要进行 token 验证。token 验证失败的时候会抛出401错误,因此需要添加错误处理,而且要放在 app.use(koajwt()) 之前,否则不执行。
如果请求时没有token或者token过期,则会返回401。
解析koa-jwt
上面使用 jsonwebtoken 的 sign() 方法来生成 token 的,那么 koa-jwt 做了些什么帮我们来验证 token。
resolvers/auth-header.js
module.exports = function resolveAuthorizationHeader(ctx, opts) {
if (!ctx.header || !ctx.header.authorization) {
return;
}
const parts = ctx.header.authorization.split(' ');
if (parts.length === 2) {
const scheme = parts[0];
const credentials = parts[1];
if (/^Bearer$/i.test(scheme)) {
return credentials;
}
}
if (!opts.passthrough) {
ctx.throw(401, 'Bad Authorization header format. Format is "Authorization: Bearer <token>"');
}
};
在 auth-header.js 中,判断请求头中是否带了 authorization,如果有,将 token 从 authorization 中分离出来。如果没有 authorization,则代表了客户端没有传 token 到服务器,这时候就抛出 401 错误状态。
verify.js
const jwt = require('jsonwebtoken');
module.exports = (...args) => {
return new Promise((resolve, reject) => {
jwt.verify(...args, (error, decoded) => {
error ? reject(error) : resolve(decoded);
});
});
};
在 verify.js 中,使用 jsonwebtoken 提供的 verify() 方法进行验证返回结果。jsonwebtoken 的 sign() 方法来生成 token 的,而 verify() 方法则是用来认证和解析 token。如果 token 无效,则会在此方法被验证出来。
index.js
const decodedToken = await verify(token, secret, opts);
if (isRevoked) {
const tokenRevoked = await isRevoked(ctx, decodedToken, token);
if (tokenRevoked) {
throw new Error('Token revoked');
}
}
ctx.state[key] = decodedToken; // 这里的key = 'user'
if (tokenKey) {
ctx.state[tokenKey] = token;
}
在 index.js 中,调用 verify.js 的方法进行验证并解析 token,拿到上面进行 sign() 的数据 {name: result.name, _id: result._id},并赋值给 ctx.state.user,在控制器中便可以直接通过 ctx.state.user 拿到 name 和 _id。
安全性
- 如果 JWT 的加密密钥泄露的话,那么就可以通过密钥生成 token,随意的请求 API 了。因此密钥绝对不能存在前端代码中,不然很容易就能被找到。
- 在 HTTP 请求中,token 放在 header 中,中间者很容易可以通过抓包工具抓取到 header 里的数据。而 HTTPS 即使能被抓包,但是它是加密传输的,所以也拿不到 token,就会相对安全了。
bcryptjs密码加密
npm install bcryptjs --save
同步用法(Sync)
生成hash密码
const bcrypt = require('bcryptjs');
const salt = bcrypt.genSaltSync(10);
// hash 加密后的密码 "B4c0/\/"加密前的密码
var hash = bcrypt.hashSync("B4c0/\/", salt);
密码验证
bcrypt.compareSync("B4c0/\/", hash); // true
bcrypt.compareSync("not_bacon", hash); // false
快速生成hash值
var hash = bcrypt.hashSync('bacon', 8);
异步用法(Async)
生成hash密码
var bcrypt = require('bcryptjs');
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash("B4c0/\/", salt, function(err, hash) {
// Store hash in your password DB.
});
});
密码验证
// Load hash from your password DB.
bcrypt.compare("B4c0/\/", hash, function(err, res) {
// res === true
});
bcrypt.compare("not_bacon", hash, function(err, res) {
// res === false
});
// As of bcryptjs 2.4.0, compare returns a promise if callback is omitted:
bcrypt.compare("B4c0/\/", hash).then((res) => {
// res === true
});
快速生成hash值
bcrypt.hash('bacon', 8, function(err, hash) {
});