文章目录
系列文章目录
使用eggjs+微信小程序开发一个时间管理小程序(一)——项目介绍
使用eggjs+微信小程序开发一个时间管理小程序(二)——后端项目搭建
使用eggjs+微信小程序开发一个时间管理小程序(三)——前端项目搭建
使用eggjs+微信小程序开发一个时间管理小程序(四)——自动登录实现
本章要点
上一篇文章介绍了小程序【轻记时刻】的基本功能,接下来我将从后端项目搭建开始,对涉及的知识点详细展开分享。
- 创建项目
- 目录结构
- 基础配置
- mysql和jwt
- router、controller、service的开发约定
一、创建项目
后端我用的是nodejs,采用了阿里的eggjs框架。
Egg 是阿里巴巴基于 Koa 的有约束和规范的企业级 Web 开发框架,基于 Egg 的项目目录结构和名称有严格的规定,和 ESLint 一样,如果不符合规定那么项目将无法运行,此外,Egg 基于 MVC 的架构模式,M —— Model 层负责应用程序的数据逻辑部分,类似于 Service、V —— View 层负责应用程序的数据显示部分(静态/动态网页),类似于 Router、C —— Controller 层负责应用程序的业务逻辑部分,将数据和页面关联
我有一个开源的eggjs搭建的项目,结构与该项目基本一致,放在gitee上,大伙儿有兴趣可以下载参考下。
// 创建一个文件夹
mkdir clock
cd clock
// 初始化一个node项目
npm int -y
// 安装egg包
npm i -S egg
npm i -D egg-bin
在package.json的script中添加一句
"dev": "egg-bin dev"
之后我们将使用npm run dev
命令来启动项目。
二、目录结构
egg项目需要按照规范的文件目录来建立,否则启动时会报错
我们先依照上图创建必要的文件夹和文件(上图已做标记)
三、基础配置
config.**.js
表示不同环境下的配置,default
是在各个环境下都生效的配置,实际上某个环境的最终配置,是default
和相应环境配置的合并。
配置中必须要有keys
这个属性,其值是一个自定义的字符串,所以我们写在config.default.js
中。
config
有多种exports
的方法,具体可以参见官方文档,写的很详细。
// config/config.default.js
exports.keys = 'clock';
// 服务的端口号 不设置的话,默认是7001
exports.cluster = {
listen: {
path: '',
port: 8888,
hostname: '0.0.0.0'
}
}
csrf
配置
egg
框架内置了安全系统,默认开启防止XSS
攻击 和CSRF
攻击,每次请求得时候请求头必须携带csrfToken
字段,通过该配置,指定了header
的字段名是x-csrf-token
- 当发起请求时,前端可以通过
cookie
获取eggjs
服务生成的csrfToken
,然后将该值传到header
中的x-csrf-token
中,才能通过egg
校验,成功发起请求。- 不过小程序没有
cookie
,所以我们将enable
设置为false
,禁用该安全系统
// config/config.default.js
exports.security = {
csrf: {
enable: false, // 禁用这个
headerName: 'x-csrf-token'
}
}
router.js中定义一个路由规则
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/home/index', controller.home.index); // 首页
router.redirect('/', '/home/index', 302); // 当没有指定路由的时候,重定向到首页
}
// app/controller/home.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() { // 首页
this.ctx.body = 'hello world';
}
}
module.exports = HomeController;
有以上代码,项目就可以跑起来了,执行npm run dev
,访问http://127.0.0.1:7001
可以看到,路由被重定向到了http://127.0.0.1:7001/home/index
, 页面上显示hello world
。说明我们的项目已经成功搭建并运行起来了
四、MySQL和JWT
1. MySQL
- 首先要安装
mysql
和navicat
(可视化的数据库管理工具)
大家可以去官网下载,mysql
推荐用社区版,不收费。navicat
下载后需要破解,百度很容易找到攻略,相信难不倒你 - 软件安装完毕后,在
egg
项目中使用mysql
,还得安装egg-mysql
这个包
npm i -S egg-mysql
- 在
config
中添加mysql
相关配置,plugin.js
中指定插件的使用情况
// config/config.default.js中添加一段
exports.mysql = {
client: {
// host
host: 'localhost',
// 端口号
port: '3306',
// 用户名
user: 'root',
// 密码
password: '你的密码',
// 数据库名
database: 'clock',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
// config/plugin.js
exports.mysql = {
enable: true, // 是否启用mysql插件
package: 'egg-mysql', // 插件对应的node包
};
在业务代码中使用MySQL
async testFn(data) {
// 开启事务
const result = await this.app.mysql.beginTransactionScope(async conn => {
// 查询
const res = await conn.select('invite', {
where: {
invite_code: data.code
}
});
// 或者直接写sql
const res = await conn.query(`select * from invite where invit_code=${data.code}`);
}, this.ctx);
return result;
}
2. JWT
- JWT是json web token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
- jwt提供了
sign
和verify
两个方法,sign
用于签发token,verify
用于校验token是否有效。
签发的代码可以参考上面service/user.js中的login方法,签发时可以传入一些非敏感用户信息,在校验的时候可以被解析出来,用于确认当前访问的用户。- 关于JWT的详细解析可以点这里,就不展开了。
JWT的安装和使用
服务端要支持CORS(跨来源资源共享)
策略
npm i -S jsonwebtoken
npm i -S egg-cors
启用插件
// config/plugin.js 增加一段
exports.cors = {
enable: true,
package: 'egg-cors'
}
// config/config.default.js 增加一段
exports.cors = {
origin: '*', // 不限制来源
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
}
exports.jwt = {
enable: true,
secret: 'yourPrivateKey', // 自定义的秘钥,用于token的签发和校验,不应该流露出去
expiresIn: 60 * 60 // 有效时间,单位秒
};
JWT的工作流程
- 登录时,当账号密码校验通过后,使用JWT签发生成token
// 登录成功,签发jwt
const token = JWT.sign({
userId: '当前登录用户的userId',
}, this.config.jwt.secret, {
expiresIn: this.config.jwt.expiresIn
});
token的生成原理
JWT
生成的token
是由三部分组成的字符串,大体结构是"header.payload.signature"
。
- header
header
通常由两部分组成:令牌的类型,即JWT
;常用的散列算法,如HMAC SHA256
或RSA
。
例如:
{ "arg": "SHA256", "typ": "JWT" }
这部分的JSON
被base64
编码,形成token
的第一部分- payload
这里放一些非敏感信息,这块也被base64
编码,形成token
第二部分
这一块就是JWT.sign
方法的第一个参数,上面示例是传入了userId
signature
用来验证发送请求者身份,由前两部分base64的结果用.
拼接后,在用指定的加密算法,以jwt配置中的密钥进行加密生成。
HMACSHA256(base64Encode(header) + '.' + base64Encode(payload), secret)
- 登录接口将生成的token返回给前端,前端在之后的请求中,将该token放在请求头header的某个字段中。
- 服务端的某些接口如果需要在登录状态下,则需要验证这个token是否有效,要用到
JWT.verify
方法 - 封装登录状态校验方法
// extend/helper.js
checkLogin(app) {
let obj = {
result: false,
body: {}
};
// 拿到请求体header中authorization中存储的token
const token = app.ctx.request.header.authorization;
if (!token) { // 不存在token
obj = {
result: false,
body: {
status: 401,
message: '未登录, 请先登录',
result: null
}
}
} else { // 存在token
let decode;
try {
// 验证当前token
decode = JWT.verify(token, app.config.jwt.secret);
app.ctx.user = decode;
obj = {
result: true,
body: {
result: decode
}
}
} catch (e) {
obj = {
result: false,
body: {
status: 401,
message: '登录信息已过期,请重新登录',
result: null
}
}
}
}
return obj;
}
后面将说使用方法
五、router、controller、service的开发约定
1. router
router.js
中定义对外暴露的接口
module.exports = app => {
const { router, controller } = app;
router.get('/user/index', controller.user.index); // 首页
router.redirect('/', '/user/index', 302); // 当没有指定路由的时候,重定向到首页
// 用户相关
router.post('/user/login', controller.user.login); // 用户登录
router.post('/user/getUserInfo', controller.user.getUserInfo); // 查用户详情
}
router.get / router.post
有两个参数,第一个是请求地址,第二个是对应处理的方法,我们都指向相应controller
下面的方法中。get/post
对应的是需要的请求method
2. controller
一个服务一个文件,我们以user服务为例
// controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async index() { // 首页
this.ctx.body = 'hello world';
}
async login() { // 用户登录
const res = await this.ctx.service.user.login();
this.ctx.body = res;
this.ctx.status = res.status;
}
async getUserInfo() { // 获取用户信息
const checkResult = this.ctx.helper.checkLogin(this);
if (checkResult.result) {
const res = await this.ctx.service.user.getUserInfo();
this.ctx.body = res;
this.ctx.status = res.status;
} else {
this.ctx.body = checkResult.body;
this.ctx.status = checkResult.body.status;
}
}
}
module.exports = UserController;
我们在controller
里会按需校验登录信息,比如getUserInfo
这个方法需要登录情况下才能调用,所以要先调用上面提到的checkLogin
方法,先验证下token
是否有效。如果无效直接返回,如果有效则调用service
里面的方法。比如login
不需要登录就能调用,则不用checkResult
。
3. service
和controller
一样,一个服务一个文件
service
负责输入输出操作,对数据库的操作等。基础的业务逻辑校验可以放在controller
。
// service/user.js
'use strict';
const Service = require('egg').Service;
class UserService extends Service {
async getUserInfo() { // 获取用户信息
// 开启事务
const result = await this.app.mysql.beginTransactionScope(async conn => {
try {
const queryUser = await conn.select('user', {
where: {
open_id: this.ctx.user.openid
}
});
if (queryUser && queryUser.length) {
return {
message: '查询成功',
status: 200,
result: queryUser[0]
}
} else {
return {
message: '用户不存在',
status: 500,
result: null
}
}
} catch (err) {
return {
message: '系统异常,请稍后再试',
status: 501,
result: null
}
}
});
return result;
}
}
module.exports = UserService;
这里的return的内容,要用相同的结构,方便统一处理。
总结
本文详细介绍了后端项目的搭建过程,并对mysql、jwt的安装和使用进行了说明,希望对大家有帮助。下一篇我们将介绍前端项目的搭建。