用戶系統是許多網站的基礎。這篇文章主要就是講解如何寫一個基於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
的網站,有完整的用戶系統,若是沒有什麼頭緒的話能夠參考一下~
若是文章中有什麼錯誤或者不妥的地方,歡迎指出,互相交流學習~感謝閱讀~