在上一篇文章,咱们已经使用Sequelize链接上了数据库,并能进行简单的数据库操做,在此基础上,咱们试着来开发一个完整的项目。这篇文章咱们从用户的注册、登陆着手,试着开发用户模块的相关的代码。javascript
用户注册的逻辑很简单:html
客户端用户提交数据后,服务端验证经过进行数据库写入,可是其中用户密码是敏感信息,为了服务安全考虑,不能直接将明文密码写入到数据库,防止数据库被攻击,用户密码泄露。因此通常在存储用户密码时,会先对用户密码进行加盐hash处理,这样哪怕数据库存储的密码泄露,其余人也没法经过处理后的密码进行登陆。这里使用哈希算法对密码进行处理,由于哈希的特性是不可逆,具体的细节能够参考为何说 MD5 是不可逆的?。java
这里咱们对密码的处理方法叫哈希,即hash,它不是一种加密算法,而是一种摘要算法。网上不少文章对这两个概念都混淆了,其实加密跟哈希是彻底不一样的两个概念。具体可参考:哈希(Hash)与加密(Encrypt)的基本原理、区别及工程应用。web
哈希能够被暴力破解,加盐能够很大程度上增长破解难度。因此对密码的处理方式通常是每一个用户密码对应一个随机盐,跟密码存储在一块儿,这样就算是脱库了,别人想破解也很难,由于要针对每一个密码去破解。因此最后咱们生成hash的公式为:算法
R = H(password + randomSalt)
若是你还以为不安全,你甚至还能够在代码里再写一个固定盐值,用两个盐进行hash,或者屡次hash等等...shell
废话很少说,咱们下面来写代码。数据库
首先,咱们安装一下bcryptjs
,咱们使用它对密码进行加盐哈希和比对:npm
npm install bcryptjs --save
而后咱们把这两个方法写到app/extend/helper.js
:json
const bcrypt = require('bcryptjs'); module.exports = { encrypt(password) { const salt = bcrypt.genSaltSync(10); //加盐 const hash = bcrypt.hashSync(password, salt); //哈希(同步调用) return hash; }, compare(password, hash) { return bcrypt.compareSync(password, hash); //比对 } };
这里你可能会有点疑问:为何bcrypt.compareSync
方法没有salt的入参呢?这里说明一下,bcrypt.hashSync
在生成hash时已经将盐值存储在结果中了,因此他算出来的结果是hash串和salt的结合,这样咱们就不用往数据库存储盐值了,比较方便。安全
这样咱们在项目里就能够经过this.ctx.helop.encrypt
和this.ctx.helop.compare
的方式去使用这些公用的方法了。
而后,在UserController
中添加一个register
方法:
async register() { const params = this.ctx.request.body; // 参数校验 if (!params.name || !params.password || !params.phone || !params.email) { this.ctx.body = { code: '500', msg: '参数不合法' }; } // 查询该用户是否已经注册 const user = await this.ctx.model.User.findOne({ where: { name: params.name } }); if (user) { this.ctx.body = { code: '500', msg: '该用户已存在' }; } // 插入数据库 const result = await this.ctx.model.User.create({ ...params, password: this.ctx.helper.encrypt(params.password) }); if (result) { this.ctx.body = { code: '200', msg: '注册成功' }; } }
最后,添加路由:
// app/router.js router.post('/register', controller.user.register);
测试一下,用postman建立一个post请求到localhost:7001/register
,传入注册用户信息:
{ "name":"xiaoming", "password":"test1234", "phone":"13412341234", "email":"test@gmai.com" }
服务端返回“注册成功”的提示语,这时候咱们去数据库就能看到这条数据了,并且密码是一坨看不懂的密文,这个时候咱们用一样的数据再次发起请求,服务端返回“该用户已注册”,说明咱们的注册功能已经完成了!
用户登陆的逻辑很简单,就是一个客户端传入的用户名密码与服务器存储的相比较,匹配就登陆成功,否则就是用户名密码错误。可是,咱们还须要额外考虑一个问题,http请求是无状态的,那咱们怎么去记住用户的登陆状态和登陆信息呢,而且每次向服务端发起请求都带上这些信息呢?
经常使用的用户认证方式有两种,一种是经过Cookie实现,一种是Token的实现方式。关于用户受权认证能够看看这篇文章:受权认证登陆之 Cookie、Session、Token、JWT 详解,Eggjs官网也有一个章节讲了Cookie和Session相关的知识Cookie 与 Session,学习服务端,掌握这些知识仍是很必要的。
在这里,咱们选择JWT做为咱们的解决方案。关于JWT,这里提供两篇文章做为参考,JSON Web Token 入门教程,Introduction to JSON Web Tokens。OK,原理看完之后咱们来写代码。
首先咱们安装egg-jwt
插件:
npm install egg-jwt --save
引入插件:
// app/config/plugin.js jwt: { enable: true, package: "egg-jwt" }
配置jwt secret:
// app/config/config.default.js config.jwt = { secret: '12312456 };
OK,咱们下面编写代码的代码:
// app/controller/user.js async login() { const params = this.ctx.request.body; if (!params.name || !params.password) { this.ctx.body = { code: '500', msg: '参数不合法' } } const user = await this.ctx.model.User.findOne({ where: { name: params.name } }); if (!user) { this.ctx.body= { code: '500', msg: '用户名密码错误' } } //校验密码 const checkPassword = this.ctx.helper.compare( params.password, user.password ); if (checkPassword) { // 根据用户名称建立token,过时时间为2小时 const token = this.app.jwt.sign({ name: user.name }, this.app.config.jwt.secret, { expiresIn: '2h' }); this.ctx.body = { code: '200', data: token } } else { this.ctx.body = { code: '500', msg: '用户名密码错误' } } }
配置路由:
// app/router.js router.post('/login', controller.user.login);
代码完毕,让咱们测试一下,建立一个post请求到localhost:7001/login
,输入用户名密码:
{ "name":"xiaoming", "password":"test1234" }
结果以下:
{ "code": "200", "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb21pbmciLCJpYXQiOjE2MDg1NTkzMTYsImV4cCI6MTYwODU2NjUxNn0.SyXAhVvrwAql-4FzaZrlEs6dsEJ4wXbdjQsHv43CSOI" }
成功了,这一大坨就是咱们建立的token。
嗯……咱们是拿到了token,可是怎么用呢?还记得咱们最开始写的两个接口吗?咱们如今给他配置jwt验证,而后试试不登陆能不能访问。
// app/router.js router.post('/createUser', app.jwt, controller.user.createUser); router.get('/getUsers', app.jwt, controller.user.getUsers);
而后咱们从新测试一下这两个接口,服务端返回401 Unauthorized
,这说明验证生效了。那咱们在请求的时候把刚才登陆接口获取到的token设置在Authorization
的Bearer Token
字段中,再试一次,成功了!
若是你跟我走到了这一步,那说明你已经距离一个合格的服务端开发者更近了一步,可是这里咱们好像并无用到token
里面携带的信息,下一篇咱们将会解析token
携带的信息,知道每次都是哪一个用户在访问咱们的服务,并且咱们将优化咱们的代码,让它看起来更简洁、合理。