用户系统是许多网站的基础。这篇文章主要就是讲解如何写一个基于Node
的单页应用的用户系统,这个用户系统的功能包括:注册,登陆,自动登陆,忘记密码,修改密码,邮件激活。
若是使用在后端使用模板引擎,而不是用先后端分离的方案,用户系统貌似没有那么复杂。在这个Nodejs教程里面已经介绍得很详细了(这是个不错的Nodejs
教程)。可是若是选择先后端分离的方案,好比像接下来要介绍的SPA
,那用户系统又该怎么处理呢?模板引擎的方案里面,事实上session/cookie
上都作了封装,因此操做起来相对简单。但后者则不同,它须要咱们对于HTTP
相关的概念有更加清晰的认识。要求会更加细致。html
下面先介绍一下一些基础的知识。说得不会不少,可是对于完全理解Cookie
,Session
整个Authentication
的机制很是重要。前端
众所周知,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.sid
的cookie
。这个cookie
是随机生成的独一无二的序列码,每次用户发起请求的时候,cookie
跟着到了服务器上去。服务器检查一下用户的connect.sid
,而后从内存,缓存,数据库或者Redis
里面找到相应的信息,而后经过中间件进一步加到请求里面。这样服务器就可使用专属于这个用户的信息而再也不须要屡次验证了。
所以cookie
是整个用户机制的核心,下面简单介绍一下相关的header
。node
Set-Cookie
是request
的header
。header
的格式是NAME=VALUE
而后用分号‘;’分隔开来。
其中有几个设置比较经常使用:git
expires=Date
(设置cookie
的到期时间)github
secure
(仅仅只在https
下使用)ajax
HttpOnly
(使得cookie
不能被客户端JavaScript
修改)mongodb
maxAge
(cookie
的保持时间,以毫秒为单位)数据库
读取和设置cookie
在Nodejs
里面都很方便,在Express
里面添加中间件cookie-parser
,能够把cookie
对象直接赋给req
。在路由回调函数里面操做的时候,直接用req.cookie
就能够获取到客户端的cookie
值。
而设置客户端的cookie
则须要用res.cookie
函数来设置:express
// 把cookie里面的name值设为name res.cookie('name', name, { maxAge: 1000 * 60 * 60 * 24 * 30, path:'/', httpOnly: false })
Express
的session
实现须要一个中间件:segmentfault
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
这一项。这样就完成了一次区分,而不须要再次验证。
在这里的预设是要作一个单页应用。若是使用模板引擎,使用render
很容易就能够完成登陆等等的功能,但若是要写一个先后端分离的应用,好比一个SPA
,那就不得不使用AJAX
来收发用户信息。
无论使用什么库来收发AJAX
,有一点是须要注意的:那就是发送的AJAX请求要包含credentials: 'include'
以保证cookie
可以被携带发送到后端,不然后端的req.cookie
不会收到。
对于须要确认用户已经登陆了才可以使用的路由,须要加一个中间件。这个中间件的做用是检查req.session.user
是否是已经定义了。通常来讲,在用户登陆以后都须要设置一下req.session.user
,以表示处于登陆的状态。
function authorize(req, res, next) { if(req.session.user) { next() } else { res.status(401).send({errorMsg: "Unauthorize"}) } }
对于一个注册的过程来讲须要有以下的一些步骤。收到用户的用户名,邮箱以后,要在数据库里面找一下,若是找到了同名或者用邮箱的,就要告知用户,重名了。若是没有重名,就发送邮件到邮箱中进行验证,同时建立一个未激活的帐户。
另外一个要注意的点就是密码的存取最好不要直接存入,推荐是先加密。
这里涉及到了多重嵌套的异步,可使用我以前写的这篇文章的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,请忽略此邮件。点击下面连接激活帐户。<br>\ <a href=' + link + ' target="_blank">激活连接</a>', 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)) }
用户登陆须要有如下的步骤,代码就不详细叙述了。这里面须要很是繁琐的判断语句,可是理解起来很是简单。
激活用户须要用到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); } }) }
固然,这一个用户登陆系统仍然还有不少要改进的地方(好比安全问题等等)。除此以外,在功能上还有很多须要增长的。好比修改密码,好比更换密码等等,看了上面的内容,其实要完成这些功能也是很是简单的一件事了。
若是感兴趣的话能够看看我本身写的一个网站,Colors,这是一个基于React
和Nodejs
的网站,有完整的用户系统,若是没有什么头绪的话能够参考一下~
若是文章中有什么错误或者不妥的地方,欢迎指出,互相交流学习~感谢阅读~