如何用Eggjs從零開始開發一個項目(2)

在上一篇文章,咱們已經使用Sequelize鏈接上了數據庫,並能進行簡單的數據庫操做,在此基礎上,咱們試着來開發一個完整的項目。這篇文章咱們從用戶的註冊、登陸着手,試着開發用戶模塊的相關的代碼。javascript

用戶註冊

1. 註冊邏輯

用戶註冊的邏輯很簡單:html

  • 客戶端:用戶輸入輸入帳號,密碼等信息進行用戶註冊;
  • 服務端:接收到客戶端提交的註冊信息後,進行字段的檢驗(是否必填、字段長度等),字段符合要求後,根據用戶註冊的帳號查詢數據庫,根據返回結果判斷該用戶是不是新用戶,若是是新用戶,將用戶信息寫入到數據庫,完成註冊流程。

2. 用戶密碼處理

客戶端用戶提交數據後,服務端驗證經過進行數據庫寫入,可是其中用戶密碼是敏感信息,爲了服務安全考慮,不能直接將明文密碼寫入到數據庫,防止數據庫被攻擊,用戶密碼泄露。因此通常在存儲用戶密碼時,會先對用戶密碼進行加鹽hash處理,這樣哪怕數據庫存儲的密碼泄露,其餘人也沒法經過處理後的密碼進行登陸。這裏使用哈希算法對密碼進行處理,由於哈希的特性是不可逆,具體的細節能夠參考爲何說 MD5 是不可逆的?java

這裏咱們對密碼的處理方法叫哈希,即hash,它不是一種加密算法,而是一種摘要算法。網上不少文章對這兩個概念都混淆了,其實加密跟哈希是徹底不一樣的兩個概念。具體可參考:哈希(Hash)與加密(Encrypt)的基本原理、區別及工程應用web

哈希能夠被暴力破解,加鹽能夠很大程度上增長破解難度。因此對密碼的處理方式通常是每一個用戶密碼對應一個隨機鹽,跟密碼存儲在一塊兒,這樣就算是脫庫了,別人想破解也很難,由於要針對每一個密碼去破解。因此最後咱們生成hash的公式爲:算法

R = H(password + randomSalt)

若是你還以爲不安全,你甚至還能夠在代碼裏再寫一個固定鹽值,用兩個鹽進行hash,或者屢次hash等等...shell

廢話很少說,咱們下面來寫代碼。數據庫

首先,咱們安裝一下bcryptjs,咱們使用它對密碼進行加鹽哈希和比對:npm

npm install bcryptjs --save

而後咱們把這兩個方法寫到app/extend/helper.jsjson

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.encryptthis.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);

3.功能測試

測試一下,用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設置在AuthorizationBearer Token字段中,再試一次,成功了!

若是你跟我走到了這一步,那說明你已經距離一個合格的服務端開發者更近了一步,可是這裏咱們好像並無用到token裏面攜帶的信息,下一篇咱們將會解析token攜帶的信息,知道每次都是哪一個用戶在訪問咱們的服務,並且咱們將優化咱們的代碼,讓它看起來更簡潔、合理。

相關文章
相關標籤/搜索