执行过程,洋葱模型
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next()
console.log(2);
})
app.use(async (ctx, next) => {
console.log(3);
await next()
console.log(4);
ctx.body = "hello koa2!!!"
})
app.use(ctx => {
console.log(5);
ctx.body = "hello koa3"
})
app.listen(3000)
打印结果 1 ,3, 5, 4, 2
路由中间件
根据url匹配处理
处理不同的url
通过不同的url路径,跳转不同的页面
app.use(async (ctx, next) => {
if(ctx.url === '/'){
ctx.body = "主页"
}else if(ctx.url === "/users"){
ctx.body = "用户列表"
}else{
ctx.status = 404;
}
})
处理不同的http请求方式
if(ctx.url === "/users"){
if(ctx.method === "GET"){
ctx.body = "用户列表"
}
else if(ctx.method === "POST"){
ctx.body = "创建用户"
}
else{
// 不允许操作
ctx.status = 405;
}
}
解析url上的参数
使用url.match()
if(ctx.url.match(/\/users\/\w+/)){
const userId = ctx.url.match(/\/users\/(\w+)/)[1];
ctx.body = `用户${userId}`
}
通过koa-router中间件
const Router = require('koa-router')
const router = new Router()
router.get('/',(ctx) =>{
ctx.body = "这是主页"
})
router.get('/users', (ctx) =>{
ctx.body = "用户列表"
})
// 不同的http请求方法
router.post('/users', (ctx) =>{
ctx.body = "创建用户"
})
// 获取url路径上的参数
router.get('/users/:id', (ctx) =>{
ctx.body = `这是用户${ctx.params.id}`
})
app.use(router.routes())
路由添加前缀prefix
// 添加前缀,可以分层级管理路由
const userRouter = new Router({prefix: "/users"})
userRouter.get('/', (ctx) =>{
ctx.body = "用户列表"
})
// 不同的http请求方法
userRouter.post('/', (ctx) =>{
ctx.body = "创建用户"
})
// 获取url路径上的参数
userRouter.get('/:id', (ctx) =>{
ctx.body = `这是用户${ctx.params.id}`
})
app.use(userRouter.routes())
使用koa-bodyparser解析body内容
const bodyparser = require("koa-bodyparser");
// 注册到koa实例app上
app.use(bodyparser());
重构路由结构
把home和users抽离为单独路由
home.js代码
const Router = require("koa-router");
const router = new Router()
router.get('/', (ctx)=>{
ctx.body = "这里是主页"
})
module.exports = router;
users.js文件
const Router = require("koa-router");
const router = new Router({prefix:"/users"})
router.get('/', (ctx) =>{
ctx.body = "用户列表"
})
// 不同的http请求方法
router.post('/', (ctx) =>{
ctx.body = "创建用户"
})
// 获取url路径上的参数
router.get('/:id', (ctx) =>{
ctx.body = `这是用户${ctx.params.id}`
})
module.exports = router;
index文件收集所有路由文件统一处理
使用到node的fs模块,同步读取目录下的所有文件。将所有文件统一做处理导出。
const fs = require('fs');
const allRoutes = (app)=>{
fs.readdirSync(__dirname).forEach(file=>{
if(!file.includes("index")){
const route = require(`./${file}`)
app.use(route.routes()).use(route.allowedMethods())
}
})
}
module.exports = allRoutes;
然后在入口文件引入routes目录下的index文件
const routes = require("./routes")
routes(app);
控制器control
处理经过路由之后进入的页面,处理不同的业务逻辑。由于业务逻辑比较复杂,实际项目开发中,会把控制器单独抽离出来。
创建controllers目录
home控制器
class HomeCtrl{
index(ctx){
ctx.body = "这是首页page"
}
}
module.exports = new HomeCtrl();
user控制器
class UsersCtrl{
findUser(ctx){
ctx.body = "用户列表"
}
findUserById(ctx){
ctx.body = `这是用户${ctx.params.id}`
}
createUser(ctx){
ctx.body = "创建用户"
}
}
module.exports = new UsersCtrl();
错误处理
koa内部处理
- 404,客户端请求路径错误
- 412,获取特定数据不存在,即:先决条件失败412Precondition Failed
500,服务器内部错误,运行时错误(不是语法错误)。
findUserById(ctx){
if(ctx.params.id >100){
ctx.throw(412, "先决条件失败,查询数据不存在。")
}
ctx.body = `这是用户${ctx.params.id}`
}
编写中间件处理错误
由于koa内部处理错误返回的数据不是json格式,需要通过中间件处理。
app.use(async (ctx,next) => {
try {
await next();
}catch (err){
ctx.status = err.status || err.statusCode ||500;
ctx.body = {
message: err.message
};
}
})
错误处理第三方中间件
koa-json-error : 处理报错信息为json,生产环境设置取消stack的打印
-
用户认证JWT
JWT介绍
JSON WEB TOKEN的简写,定义了将各方之间的信息作为json对象进行安全传输,该信息可以验证和信任,信息都是经过数字签名的。
JWT的构成:头部header、有效载荷payload、签名signature。
红色为header,紫色为payload,青色为signature
https://www.jianshu.com/p/576dbf44b2aejwt的头部header承载两部分信息
声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
将头部进行base64加密,构成了第一部分header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload载荷
载荷就是存放有效信息的地方。有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature算法
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
var signature = HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload)
, 'secret');
将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt。
node中使用JWT
- 安装jsonwebtoken
- 签名
- 验证
const jwt =require("jsonwebtoken")
const token = jwt.sign({name:"sam"}, "secret")
jwt.verify(token, "secret")
用jwt实现用户注册和登录
1:当用户登录时,生产token信息,并返回给前端
// index首页,使用koa-parameter对客户端输入的数据进行校验
const parameter = require('koa-parameter');
parameter(app);
const jsonwebtoken = require("jsonwebtoken");
async login(ctx){
// ctx.request.body name和password必须填写,parameter对参数进行的校验
ctx.verifyParams({
name: {type: "string", required: true},
password: {type: "string", required: true}
})
// 查询输入的用户是否存在
const user = await User.findOne(ctx.request.body);
if(!user) {ctx.throw(401, '用户名或密码有误');}
//解析出用户名和_id信息
const {_id, name} = user;
// 设置jsonwebtoken,使用sign签名,第一个参数要校验的数据,第二个secret密码,第三有效时长
const token = jsonwebtoken.sign({_id, name}, "secret_zidingyi_y8e42938", {expiresIn:"1d"});
// 将生成后的token返回前端,前端以后进行接口请求时,需要携带此信息。
ctx.body = {token}
}
2:编写一个认证的中间件
认证的作用,用户进行相关操作时进行的身份认证,看当前作用的身份是否正确,是否能调用当前接口。
通常需要对用户的更新进行认证操作,如果登录的身份和更新的身份不一致,则不能进行更新操作。// 自定义认证中间件
const auth = async (ctx, next) =>{
const {authorization = ""} = ctx.request.header;
const token = authorization.replace("Bearer ", "")
try{
const user = jsonwebtoken.verify(token, "secret_zidingyi_y8e42938");
// 将用户信息存储
ctx.state.user = user;
}catch(err){
ctx.throw(401, err.message)
}
await next();
}
// 更新用户信息,新进行认证操作,成功后在进行更新操作
router.put('/:id', auth, userCtrl.updateUser)
3:授权中间件,判断登录用户的权限
上述第二步中,如果登录的认证可以通过,如果需要更新其他用户的数据,就需要权限设置,如果登录用户是管理员,权限比较大,就可以更新别的用户信息,此时用到认证后的授权操作。
示例演示,仅设置最简单的权限,通过判断登录id和token的id是否一致。// 授权,设置权限
// 检查是否有操作数据的权限
const checkOwner=async (ctx, next)=>{
if(ctx.params.id !== ctx.state.user._id){
ctx.throw(403, '没有权限')
}
await next();
}
使用koa-jwt
上面使用了自定义的中间件,也可以使用现成的中间件完成上述过程。 ```javascript const jwt = require(“koa-jwt”);
// 设置jwt的认证,koa-jwt可以简化代码 const jwtAuth = jwt({secret:”secret_zidingyi_y8e42938”}) // 更新用户信息,先进行认证,在进行授权 router.put(‘/:id’, jwtAuth, checkOwner, userCtrl.updateUser)
<a name="HJE83"></a>
## 图片上传及设置静态路径
<a name="FSVPb"></a>
### koa-body中间件设置上传
koa-body可以解析上传的文件,也可以解析form及json格式。
```javascript
const koaBody = require('koa-body')
// 使用koa-body替换bodyParser,koa-body可以解析文件格式
app.use(koaBody({
multipart: true,
formidable:{
uploadDir: path.join(__dirname, "/public/uploads"), //设置上传路径
keepExtensions: true, // 保留扩展名
}
}))
koa-static设置静态目录
可以将目录设置为通过路径就能访问的静态目录
// 设置静态目录z中间件
const koaStatic = require('koa-static');
// 设置静态目录
app.use(koaStatic(path.join(__dirname, '/public')))
修改上传接口返回的路径
upload(ctx){
const file = ctx.request.files.file;
const basename = path.basename(file.path)
ctx.body = {
url: `${ctx.origin}/uploads/${basename}`
}
}
使用mongoose连接数据库
使用mongoose的connect连接数据库
const mongoose = require('mongoose')
const connectionPath="mongodb://test:123456@0.0.0.0:27017/test"
mongoose.connect(connectionPath, { useNewUrlParser: true, useUnifiedTopology: true}, () =>{
console.log('connect success')
})
mongoose.connection.on('error',(err)=>{
console.error("link error",err)
})
定义schema
定义user的模型model
const userSchema = new Schema({
"__v": {
type: Number,
// select:false,查询的时候可以不显示字段
select: false
},
"name": {
type: String,
required: true
},
"password": {
type: String,
required: true,
select: false
},
"gender": {
type: String,
// 可以枚举的类型
enum: ['male', 'female'],
// 默认的类型
default: 'male',
// 必填项
required: true
},
// 兴趣爱好,是个字符串类型的数组
"hobbies": {
type: [{
type: String,
select: false
}]
},
"employments": {
// 工作信息,是个包含多个信息对象的数组列表,包含公司/职业
type: [{
company: {
type: String
},
job: {
type: String
}
}],
select: false
},
// 粉丝列表
following:{
// 引用,设置关联到用户,使用了mongoose的数据类型,schema.Types.ObjectId
// ref,设置关联,在控制器中,可以使用populate()查询完整对象
type: [{ type: Schema.Types.ObjectId, ref: "User"}],
select: false
}
})
控制器
// 查询关注者列表
async followingList(ctx) {
//使用populate方法,需要在定义schema时设置ref关联对象。如果不设置populate,查询处理的只是id
const user = await User.findById(ctx.params.id).select("+following").populate("following")
if(!user){
ctx.throw(404)
}else{
ctx.body = user.following;
}
}