资讯专栏INFORMATION COLUMN

Node & 单页应用 来做一个完整用户系统吧!

Chaz / 2732人阅读

摘要:在路由回调函数里面操作的时候,直接用就可以获取到客户端的值。用回调函数来写后期看起来会很吃力看有没有重名的看是不是同一邮箱又想重复注册如果是以上两种情况,就发送错误信息。此账户名已经被注册。

1. 开场白

用户系统是许多网站的基础。这篇文章主要就是讲解如何写一个基于Node的单页应用的用户系统,这个用户系统的功能包括:注册,登录,自动登录,忘记密码,修改密码,邮件激活。
如果使用在后端使用模板引擎,而不是用前后端分离的方案,用户系统貌似没有那么复杂。在这个Nodejs教程里面已经介绍得很详细了(这是个不错的Nodejs教程)。但是如果选择前后端分离的方案,比如像接下来要介绍的SPA,那用户系统又该怎么处理呢?模板引擎的方案里面,事实上session/cookie上都做了封装,所以操作起来相对简单。但后者则不一样,它需要我们对于HTTP相关的概念有更加清晰的认识。要求会更加细致。

2. 基础知识

下面先介绍一下一些基础的知识。说得不会很多,但是对于彻底理解CookieSession整个Authentication的机制非常重要。

2.1 HTTP 2.1.1 Cookie & Session

众所周知,HTTP是无状态的协议。这个的意思就是说,如果发送两个完全一样的请求,那么收到的响应也会完全相同。然而在实际生活中,这明显不符合许多场景。因为每个人虽然都点击了按钮,但我是Harry,她是Clara,我们应该收到不同的内容。服务器需要对我们做出区分,这时候cookie就登场了。我发出请求,服务器在响应里面加一个Set-Cookie,到我们浏览器里设了一个cookie(点开devtool->Application->Cookies查看),下一次发送请求的时候,我的header里面就带有cookie了,服务器看到cookie,就知道我是Harry了。这样就完成了一次认证。
但是接下来还有一个问题:服务器资源极其宝贵,如果每次都认证会造成资源浪费。加之,如果我希望能够暂时性地在当前会话存储一些信息,存储在cookie会显得非常浪费。因此session就来了。
session就是当前用户的回话信息。它需要用到cookie,但不需要把所有信息都放在cookie里面,它需要的只是一个标示。
session的信息是存储在服务器上的,可以存在缓存里,数据库里或者类似Redis之类的东西里(没用过..)。举个例子,Express-session里面的session的标示是一个名字为connect.sidcookie。这个cookie是随机生成的独一无二的序列码,每次用户发起请求的时候,cookie跟着到了服务器上去。服务器检查一下用户的connect.sid,然后从内存,缓存,数据库或者Redis里面找到相应的信息,然后通过中间件进一步加到请求里面。这样服务器就可以使用专属于这个用户的信息而不再需要多次验证了。
因此cookie是整个用户机制的核心,下面简单介绍一下相关的header

2.1.2 Set-Cookie

Set-Cookierequestheaderheader的格式是NAME=VALUE然后用分号‘;’分隔开来。
其中有几个设置比较常用:

expires=Date (设置cookie的到期时间)

secure (仅仅只在https下使用)

HttpOnly (使得cookie不能被客户端JavaScript修改)

maxAgecookie的保持时间,以毫秒为单位)

2.2 Node.js 关于cookie

读取和设置cookieNodejs里面都很方便,在Express里面添加中间件cookie-parser,可以把cookie对象直接赋给req。在路由回调函数里面操作的时候,直接用req.cookie就可以获取到客户端的cookie值。
而设置客户端的cookie则需要用res.cookie函数来设置:

// 把cookie里面的name值设为name
res.cookie("name", name, {
  maxAge: 1000 * 60 * 60 * 24 * 30,
  path:"/",
  httpOnly: false
})
session机制

Expresssession实现需要一个中间件:

var session = require("express-session")
app.use(session({
    secret: settings.cookieSecret, // 设置密码“种子”
    store: new MongoStore({
      url: "mongodb://localhost/color" // 这里用了数据库存储session,如果不设置就会用内存
    }),
    resave: true,
    saveUninitialized: true
}))

有关session的使用Nodejs教程里面有介绍,具体来说,比如用户登录之后,可以设置 req.session.user = "harry", 然后之后的所有需要用到用户登录的场景都可以先判断一下req.session里面有没有user这一项。这样就完成了一次区分,而不需要再次验证。

2.3 前端

在这里的预设是要做一个单页应用。如果使用模板引擎,使用render很容易就可以完成登录等等的功能,但如果要写一个前后端分离的应用,比如一个SPA,那就不得不使用AJAX来收发用户信息。
不管使用什么库来收发AJAX,有一点是需要注意的:那就是发送的AJAX请求要包含credentials: "include" 以保证cookie能够被携带发送到后端,否则后端的req.cookie不会收到。

3. 实例讲解 3.1 确认

对于需要确认用户已经登录了才能够使用的路由,需要加一个中间件。这个中间件的作用是检查req.session.user是不是已经定义了。一般来说,在用户登录之后都需要设置一下req.session.user,以表示处于登录的状态。

function authorize(req, res, next) {
  if(req.session.user) {
    next()
  } else {
    res.status(401).send({errorMsg: "Unauthorize"})
  }
}
3.2 注册

对于一个注册的过程来说需要有如下的一些步骤。收到用户的用户名,邮箱之后,要在数据库里面找一下,如果找到了同名或者用邮箱的,就要告知用户,重名了。如果没有重名,就发送邮件到邮箱中进行验证,同时创建一个未激活的账户。
另一个要注意的点就是密码的存取最好不要直接存入,推荐是先加密。
这里涉及到了多重嵌套的异步,可以使用我之前写的这篇文章的co,也可以用async/await。用回调函数来写后期看起来会很吃力...

function *registerGen(req, res, newUser) {
  try {
    // 看有没有重名的
    const userOfSameName = yield new Promise(function(resolve, reject) {
      User.get("NAME", req.body.name, function(err, user) {
        if(err) reject(err)
        resolve(user)
      })
    })
    // 看是不是同一邮箱又想重复注册
    const userOfSameEmail = yield new Promise(function(resolve, reject) {
      User.get("EMAIL", req.body.email, function(err, user) {
        if(err) reject(err)
        resolve(user)
      })
    })
    
    // 如果是以上两种情况,就发送错误信息。
    if(userOfSameName) {
      return res.status(200).send({ errorMsg: "此账户名已经被注册。"})
    } else if (userOfSameEmail) {
      return res.status(200).send({ errorMsg: "此邮箱已经被注册。"})
    }

    // 成功的话就新建一个未激活的账户
    yield new Promise(function(resolve, reject) {
      newUser.save(function(err, user) {
        if(err) {
          console.log("Register error:" ,err)
          reject(err)
        }
        resolve(user)
      })
    })

    // 发送激活邮件
    yield new Promise(function(resolve, reject) {
      
      const nameHash = crypto.createHmac("sha256", SECRET)
                         .update(req.body.name)
                         .digest("hex")

      const emailHash = crypto.createHmac("sha256", SECRET)
                          .update(req.body.email)
                          .digest("hex")
     
      const base = "http://colors.harryfyodor.tk/activate/"

      // 打开这一段链接之后会可以通过立即发起一个ajax来更新数据库,激活账户。
      const link = `${base}${req.body.name}/${nameHash}|${emailHash}`

      User.activate({
        subject: "Colors 验证邮件",
        html: "如果您并没有注册Colors,请忽略此邮件。点击下面链接激活账户。
激活链接", to: req.body.email }, function(err) { if(err) reject(err) res.send({ ok: true }) resolve() }) }) } catch(e) { // 如果有错误就在这里发起,方便debug return res.status(500).send({ msg: "ERROR"}) console.log("Error ", e) } } function register(req, res) { // 密码需要先加密,不推荐明文存储。 var md5 = crypto.createHash("md5"), password = md5.update(req.body.password).digest("hex"); // 创建用户,这里的User是model(后端MVC的M)的一个构造函数。 var newUser = new User({ name: req.body.name, password: password, email: req.body.email }) // 用co函数来实现同步写法写异步 co(registerGen(req, res, newUser)) }
3.3 登陆

用户登录需要有以下的步骤,代码就不详细叙述了。这里面需要非常繁琐的判断语句,但是理解起来非常简单。

3.4 邮件通知

激活用户需要用到nodemailer这个库,非常方便,用起来也非常简单。可以上官网看。如果使用163邮箱作为发件的邮箱,有一点要格外注意,那就是密码处要是网易的授权密码。这一个需要在163邮箱里面自己设置,然后代码里就用那一个授权密码。这一点需要格外注意。

function sendEmail(detail, callback) {
  var config_email = {
    host: "smtp.163.com",
    post: "25",
    auth: {
      user: "example@163.com",
      pass: "**********" // 这个密码不是邮箱密码,请先到邮箱里面设置授权密码。
    }
  }

  var transporter = nodemailer.createTransport(config_email)
  var data = {
    from: config_email.auth.user,
    to: detail.to,
    subject: detail.subject,
    html: detail.html
  }

  // 异步发送邮件
  transporter.sendMail(data, function(err, info) {
    if(err) {
      console.log("SendEmail Error", err)
      callback(err)
    } else {
      console.log("Message sent:" + info.response)
      callback(null);
    }
  })
}
4.总结

当然,这一个用户登录系统仍然还有很多要改进的地方(比如安全问题等等)。除此之外,在功能上还有不少需要增加的。比如修改密码,比如更换密码等等,看了上面的内容,其实要完成这些功能也是非常简单的一件事了。
如果感兴趣的话可以看看我自己写的一个网站,Colors,这是一个基于ReactNodejs的网站,有完整的用户系统,如果没有什么头绪的话可以参考一下~

如果文章中有什么错误或者不妥的地方,欢迎指出,互相交流学习~感谢阅读~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91125.html

相关文章

  • Puppeteer 初探

    摘要:获取获取上下文句柄执行计算销毁句柄除此之外,还可以使用意为在浏览器环境执行脚本,可传入第二个参数作为句柄,而则针对选中的一个元素执行操作。 我们日常使用浏览器或者说是有头浏览器时的步骤为:启动浏览器、打开一个网页、进行交互。 无头浏览器指的是我们使用脚本来执行以上过程的浏览器,能模拟真实的浏览器使用场景。 有了无头浏览器,我们就能做包括但不限于以下事情: 对网页进行截图保存为图片或 ...

    appetizerio 评论0 收藏0
  • 一个变相的服务端渲染系统

    摘要:很多同学肯定都想过服务端渲染的问题。然而一看关于服务端渲染的文档,可能就被唬住了。啪啪啪,啪啪啪好,然后就好了,不到行的代码,我们就实现了一个通用化的服务化的单页应用服务端渲染解决方案。 前端发展到现在,SPA应该已经被应用的非常广了。可惜的是,我们前进的是快,而人家搜索引擎爬虫跟用户的浏览器设备还跟不上脚步。辛辛苦苦写好的单页应用,结果到了SEO跟浏览器兼容这一步懵逼了。 很多同学肯...

    smallStone 评论0 收藏0
  • FEDay 参会小记

    摘要:介绍微信风格的,与客户端体验一致,这个自己去微信上看吧,略。微信调试一件套,网页授权模拟集成代理远程调试。这些在微信开发者中心有介绍,略。年微信开发经验的人,终于又成为了零年开发经验的人,重新走上了踩坑之路。 showImg(https://segmentfault.com/img/bVtEd1);活动地址:http://fequan.com/2016/ 注意:英文不好,小记也带有自己...

    xcc3641 评论0 收藏0
  • (译 & 转载) 2016 JavaScript 后起之秀

    摘要:在年成为最大赢家,赢得了实现的风暴之战。和他的竞争者位列第二没有前端开发者可以忽视和它的生态系统。他的杀手级特性是探测功能,通过检查任何用户的功能,以直观的方式让开发人员检查所有端点。 2016 JavaScript 后起之秀 本文转载自:众成翻译译者:zxhycxq链接:http://www.zcfy.cc/article/2410原文:https://risingstars2016...

    darry 评论0 收藏0
  • 前端每周清单第 29 期:Web 现状分析与优化策略、Vue 单元测试、Headless Chrom

    摘要:前端每周清单第期现状分析与优化策略单元测试爬虫作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清单第 29 期:Web 现状分析与优化策略...

    HackerShell 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<