用户注册与登录

这一节开始,我就来实现具体的功能了,这一节要实现的是用户登录注册与登出。

在前一节已经规划好了UserSchema ,这儿增加了一个isAdmin 字段来判断是不是管理员

  1. ...
  2. const UserSchema = new Schema({
  3. name: {
  4. type: String,
  5. required: true, // 表示该字段是必需的
  6. unique: true // 表示该字段唯一
  7. },
  8. email: {
  9. type: String,
  10. required: true,
  11. unique: true
  12. },
  13. password: {
  14. type: 'string',
  15. required: true
  16. },
  17. isAdmin: {
  18. type: Boolean,
  19. default: false
  20. },
  21. meta: {
  22. createAt: {
  23. type: Date,
  24. default: Date.now()
  25. }
  26. }
  27. })
  28. module.exports = mongoose.model('User', UserSchema)

定义了用户表的 schema,并通过schema生成导出了 User 这个 model

由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。

思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。

Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。
所以,总结一下:
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。这里我们使用koa-session 来实对session的支持。

下载使用

  1. $ npm install --save koa-session
  1. ...
  2. const session = require('koa-session')
  3. ...
  4. app.keys = ['somethings']
  5. app.use(session({
  6. key: CONFIG.session.key,
  7. maxAge: CONFIG.session.maxAge
  8. }, app))

用户注册页面

上一节中我们已经实现了一个最简单的用户注册。来新建个views/signup.html

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. <div class="container">
  4. <div class="box sign-box">
  5. <form action="/signup" method="POST">
  6. <div class="field">
  7. <label class="label">用户名</label>
  8. <div class="control">
  9. <input class="input" name="name" type="text" placeholder="请输入用户名">
  10. </div>
  11. </div>
  12. <div class="field">
  13. <label class="label">邮箱</label>
  14. <div class="control">
  15. <input class="input" name="email" type="email" placeholder="请输入你的邮箱">
  16. </div>
  17. </div>
  18. <div class="field">
  19. <label class="label">密码</label>
  20. <div class="control">
  21. <input class="input" name="password" type="password" placeholder="请输入密码">
  22. </div>
  23. </div>
  24. <div class="field">
  25. <label class="label">重复密码</label>
  26. <div class="control">
  27. <input class="input" name="repassword" type="password" placeholder="确认你的密码">
  28. </div>
  29. </div>
  30. <div class="field is-grouped">
  31. <div class="control">
  32. <button class="button is-primary">立即注册</button>
  33. </div>
  34. <div class="control">
  35. <a href="/signin" class="button is-text">已有账号,去登录</a>
  36. </div>
  37. </div>
  38. </form>
  39. </div>
  40. </div>
  41. {% endblock %}

获取POST 请求数据

对于POST请求的处理,koa2没有封装获取参数的方法,需要我们自己去解析(通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式) 我们可以自己写,也可以直接使用第三方中间件。koa-bodyparser中间件可以把koa2上下文的formData 数据解析到ctx.request.body中

安装使用

  1. $ npm install --save koa-bodyparser
  1. // index.js
  2. ...
  3. const bodyParser = require('koa-bodyparser')
  4. ..
  5. app.use(bodyParser())

现在就可以使用ctx.request.body 获取到POST过来的参数了。

密码加密

这儿我们使用了bcryptjs 来对密码进行加密加盐。

  1. const bcrypt = require('bcryptjs')
  2. const UserModel = require('../models/user')
  3. module.exports = {
  4. async signup (ctx, next) {
  5. if (ctx.method === 'GET') {
  6. await ctx.render('signup', {
  7. title: '用户注册'
  8. })
  9. return
  10. }
  11. // 生成salt
  12. const salt = await bcrypt.genSalt(10)
  13. let { name, email, password } = ctx.request.body
  14. // TODO 合法性校验
  15. // 对密码进行加密
  16. password = await bcrypt.hash(password, salt)
  17. const user = {
  18. name,
  19. email,
  20. password
  21. }
  22. // 储存到数据库
  23. const result = await UserModel.create(user)
  24. ctx.body = result
  25. }
  26. }

用户注册

在前面这步我们已经实现了用户的注册,添加如下路由

  1. ...
  2. router.get('/signup', require('./user').signup)
  3. router.post('/signup', require('./user').signup)
  4. ..

现在访问http://localhost:3000/signup 将看见如下页面

signup

注册用户,就可以在数据库中查看到该用户。注意这儿斌没有做一些校验工作,可以自己先实现。

用户登录

现在我们来完成登录页,在routes/user.js 中新增signin方法

  1. async signin (ctx, next) {
  2. await ctx.render('signin', {
  3. title: '用户登录'
  4. })
  5. }

新建用户登录页signin.html

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. <div class="container">
  4. <div class="box sign-box">
  5. <form action="/signin" method="POST">
  6. <div class="field">
  7. <label class="label">用户名</label>
  8. <div class="control">
  9. <input class="input" name="name" type="text" autocomplete="off" placeholder="请输入用户名">
  10. </div>
  11. </div>
  12. <div class="field">
  13. <label class="label">密码</label>
  14. <div class="control">
  15. <!-- 禁止自动填充用户名密码 -->
  16. <input type="password" style="position: absolute;left: 9999999px" />
  17. <input class="input" name="password" type="password" placeholder="请输入密码">
  18. </div>
  19. </div>
  20. <div class="field is-grouped">
  21. <div class="control">
  22. <button class="button is-primary">立即登录</button>
  23. </div>
  24. <div class="control">
  25. <a href="/signup" class="button is-text">还没账号?</a>
  26. </div>
  27. </div>
  28. </form>
  29. </div>
  30. </div>
  31. {% endblock %}

新增路由

  1. // routes/index.js
  2. ...
  3. router.get('/signin', require('./user').signin)
  4. router.post('/signin', require('./user').signin)
  5. ...

现在访问http://localhost:3000/signup 将看见如下登录页

signin

用户登录时,根据post过来的name去数据库中查找有无该用户,如果有,就校验穿上来的密码与数据库中的是否一致。数据库中的密码使用了bcrypt加密。我们使用bcrypt.compare() 来比对

  1. async signin (ctx, next) {
  2. if (ctx.method === 'GET') {
  3. await ctx.render('signin', {
  4. title: '用户登录'
  5. })
  6. return
  7. }
  8. const { name, password } = ctx.request.body
  9. const user = await UserModel.findOne({ name })
  10. if (user && await bcrypt.compare(password, user.password)) {
  11. ctx.session.user = {
  12. _id: user._id,
  13. name: user.name,
  14. isAdmin: user.isAdmin,
  15. email: user.email
  16. }
  17. ctx.redirect('/')
  18. } else {
  19. ctx.body = '用户名或密码错误'
  20. }
  21. }

为了能够直观的看见我们登录了,修改一下views/header.html

  1. <nav id="navbar" class="navbar has-shadow is-spaced">
  2. <div class="container">
  3. <div class="navbar-brand">
  4. <a class="navbar-item" href="#">JS之禅</a>
  5. <div class="navbar-burger burger" data-target="navMenu">
  6. <span></span>
  7. <span></span>
  8. <span></span>
  9. </div>
  10. </div>
  11. <div id="navMenu" class="navbar-menu">
  12. <div class="navbar-start">
  13. <a class="navbar-item" href="/">主页</a>
  14. <a class="navbar-item" href="/about">关于</a>
  15. </div>
  16. <div class="navbar-end">
  17. {% if ctx.session.user %}
  18. <div class="navbar-item">
  19. {{ctx.session.user.name}}
  20. </div>
  21. <div class="navbar-item">
  22. <a href="/signout">退出</a>
  23. </div>
  24. {% else %}
  25. <div class="navbar-item">
  26. <a class="button is-small is-primary" href="/signup">注册</a>
  27. </div>
  28. <div class="navbar-item">
  29. <a class="button is-small" href="/signin">登录</a>
  30. </div>
  31. {% endif %}
  32. </div>
  33. </div>
  34. </div>
  35. </nav>

这里我们根据 session 判断用户是否登录,登录了就显示用户名以及退出按钮,如未登录则显示登录注册按钮。

在view中是不能直接获取到ctx的,除非每次都通过模板引擎传过来。为了方便,我们使用ctx.state 来将信息传给前端视图,这样我们就可以直接使用了。修改index.js在路由前面加上如下代码

  1. ..
  2. app.use(async (ctx, next) => {
  3. ctx.state.ctx = ctx
  4. await next()
  5. })
  6. router(app)
  7. ..

现在用你之前注册的用户登录试试。

login-index

用户登出

最后我们来实现用户登出 GET /signout,将session.user设置为null即可

  1. signout (ctx, next) {
  2. ctx.session = null
  3. ctx.redirect('/')
  4. }