使用jsonwebtoken完成nodejs的登陸系統

今天我們繼續,做一個簡單的登陸系統,使用jsonwebtoken作鑑權。

Mongoose添加數據庫賬號集合

首先我們定義一個AccountSchema,如下:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const AccountSchema = new Schema({
    user_id: {type: String, required: true},
    username: {type: String, required: true},
    password: {type: String, required: true},
});

const AccountModel = mongoose.model('Account', AccountSchema);
module.exports = AccountModel;

然後我們創建一個名爲account的路由,開始寫接口。先寫註冊接口,
註冊接口就是直接post account,代碼如下:

const response = require('../util/response-util');
const router = require("koa-router")();
const AccountModel = require('../model/account');
const key = require('../config/secret-key');
const md5 = require('md5');

//註冊賬號接口
router.post('/', async (ctx) => {
    let requestAccount = ctx.request.body;
    if (!requestAccount.username || requestAccount.username.length < 3) {
        ctx.throw(400, 'length of username need >= 3');
    }
    if (!requestAccount.password || requestAccount.password.length < 6) {
        ctx.throw(400, 'length of password need >= 6');
    }

    const isDuplicatedUsername = await AccountModel.findOne({'username': requestAccount.username});
    if(isDuplicatedUsername) {
        ctx.throw(400,'duplicated username');
    }

    requestAccount.password = md5(key.accountPasswordKey + requestAccount.password);
    let result = await AccountModel.create(requestAccount);
    if (result) {
        ctx.body = response.createOKResponse(result);
    } else {
        ctx.body = response.createFailedResponse(500, 'create account failed');
    }

});

賬號使用password再加一個鹽指一起做MD5加密,雖然對Schema裏做了長度、唯一性限制,在接口上也要做限制來做json返回。
再寫一個註銷接口方便寫做單元測試:

//註銷賬號接口
router.delete('/', async (ctx) => {
    let username = ctx.query.username;
    if (!username) {
        ctx.throw(400, 'need username');
    }

    let result = await  AccountModel.findOneAndDelete(username);
    if (result) ctx.body = response.createOKResponse(result);
    else ctx.body = response.createFailedResponse(500, 'delete account fail')
});

然後是登陸接口,先做校驗,生成token令牌下面再做:

//登陸接口
router.post('/user-token', async (ctx) => {
    let requestAccount = ctx.request.body;
    if (!requestAccount.username || requestAccount.username.length < 3) {
        ctx.throw(400, 'length of username need >= 3');
    }
    if (!requestAccount.password || requestAccount.password.length < 6) {
        ctx.throw(400, 'length of password need >= 6');
    }

    requestAccount.password = md5(key.accountPasswordKey + requestAccount.password);
    let account = await AccountModel.findOne(requestAccount);
    if (account) {
        ctx.body = response.createOKResponse(account);
    } else {
        ctx.body = response.createFailedResponse(500, 'query account failed');
    }

});

這些都很簡單。退出登陸不需要做,根據jwt的標準,後端不保存token,想要退出登陸前端把當前token刪掉就可以了。

使用JWT作登陸認證

現在的應用都要作多終端認證,因此使用token來做認證是非常合適的,基本上移動端都是用accsess-token來驗證用戶的登陸信息。
jsonwebtoken是一個跨域認證標準,不瞭解的朋友可以先看看阮一峯老師的這篇博客JSON Web Token 入門教程
它的好處就是可以跨域,跨平臺。而且由於服務端不需要保存token信息,開發起來非常簡單。
JWT在不同語言、平臺有不同的實現庫,由於我們是nodejs,所以直接去npm上找,node版JWT這個就是了,可以看下使用方法,真的是非常簡單呢。先運行下npm install jsonwebtoken安裝JWT依賴。
下面我們繼續寫登陸接口,返回一個user-token給客戶端,客戶端拿到後保存起來,以後的請求在請求頭裏加上token信息,後端就可以識別了。
由於我打算用加密強度非常大的RSA256加密jwt,因此先生成一對RSA密鑰,我們藉助openssl來創建RSA256密鑰對:

full-stacker/full-stacker-api/config  master ✗                                                                                                                                           2d ⚑  
▶ openssl
OpenSSL> genrsa -out jwt.pem 1024                        
Generating RSA private key, 1024 bit long modulus
....++++++
.......................++++++
e is 65537 (0x10001)
      
OpenSSL> rsa -in jwt.pem -pubout -out jwt_pub.pem
writing RSA key
OpenSSL> exit

full-stacker/full-stacker-api/config  master ✗                                                                                                                                         2d ⚑ ◒  
▶ ls
jwt.pem       jwt_pub.pem   mongo-db.js   secret-key.js

密鑰有了,我們可以在登陸接口生成jwt給客戶端了:

//登陸接口
router.post('/user-token', async (ctx) => {
    let requestAccount = ctx.request.body;
    if (!requestAccount.username || requestAccount.username.length < 3) {
        ctx.throw(400, 'length of username need >= 3');
    }
    if (!requestAccount.password || requestAccount.password.length < 6) {
        ctx.throw(400, 'length of password need >= 6');
    }

    requestAccount.password = md5(key.accountPasswordKey + requestAccount.password);
    let account = await AccountModel.findOne(requestAccount);
    if (account) {
        let cert = fs.readFileSync(path.resolve(__dirname, '../config/jwt.pem'));
        let userToken = jwt.sign({
                _id: account._id,
                username: account.username
            }, cert,
            {
                algorithm: 'RS256',
                expiresIn: '1h'
            });
        ctx.body = response.createOKResponse(userToken);
    } else {
        ctx.body = response.createFailedResponse(500, 'wrong username or password');
    }

});

這其中,fs讀取文件的時候讀相對路徑經常會找不到,無奈藉助path來讀絕對路徑給fs,jwt裏存儲用戶的id和username,這裏expiresIn表示過期時間。
最後我們寫個測試接口來測試一個jwt登陸鑑權:

//登陸鑑權測試接口
router.get('/test', async (ctx) => {
    userToken = ctx.request.get('Authorization');
    let cert = fs.readFileSync(path.resolve(__dirname, '../config/jwt_pub.pem'));

    try {
        const decoded = await jwt.verify(userToken, cert);
        ctx.body = response.createOKResponse(decoded);
    } catch (e) {
        ctx.throw(401, 'need authorization')
    }
});


這裏推薦一個測試接口的chrome插件Restlet Client,感覺比postman更好用。
好了,這下我們基本的賬號系統和登陸鑑權就做完了,使用JWT是不是超級簡單呢?

編寫單元測試

使用ava+superkoa做單元測試,代碼如下:

import test from 'ava';
import superKoa from 'superkoa';
import app from '../app';

test.serial('register account', async t => {
    let res = await superKoa(app)
        .post('/account')
        .send({username: 'test-account', password: '123456'});
    t.is(200, res.status);
    t.is(0, res.body.error);
    t.is('test-account', res.body.data.username);
});

test.serial('login && authorization', async t => {
    let res = await superKoa(app)
        .post('/account/user-token')
        .send({username: 'test-account', password: '123456'});
    t.is(200, res.status);
    t.is(0, res.body.error);

    let res2 = await superKoa(app)
        .get('/account/test')
        .set('Authorization', res.body.data);
    t.is(200, res2.status);
    t.is(0, res2.body.error);
    t.is('test-account', res2.body.data.username);
});

test.serial('unregister authorization', async t => {
    let res = await superKoa(app)
        .delete('/account?username=test-account');
    t.is(200, res.status);
    t.is(0, res.body.error);
    t.is('test-account', res.body.data.username);
});

運行一下,看看結果:

▶ npm test

> [email protected] test /Users/judy/WeChatProjects/full-stacker/full-stacker-api
> ava -v

POST /category - 424ms
  --> POST /category 200 431ms 89b
  ✔ create category (473ms)
  <-- PATCH /category
PATCH /category - 912ms
  --> PATCH /category 200 913ms 90b
  ✔ update category (920ms)
  <-- GET /category/list?parent=root
GET /category/list?parent=root - 22ms
  --> GET /category/list?parent=root 200 31ms 270b
  ✔ query categories by parent
  <-- DELETE /category?_id=test
DELETE /category?_id=test - 21ms
  --> DELETE /category?_id=test 200 22ms 90b
  ✔ delete category
  <-- POST /account
POST /account - 56ms
  --> POST /account 200 58ms 133b
  ✔ register account
  <-- POST /account/user-token
POST /account/user-token - 25ms
  --> POST /account/user-token 200 25ms 356b
  <-- GET /account/test
GET /account/test - 6ms
  --> GET /account/test 200 8ms 113b
  ✔ login && authorization
  <-- DELETE /account?username=test-account
DELETE /account?username=test-account - 15ms
  --> DELETE /account?username=test-account 200 17ms 133b
  ✔ unregister authorization
  <-- GET /
GET / - 0ms
  --> GET / 200 3ms 19b
  ✔ hello full-stacker

  8 tests passed

全部通過(有5個是之前的,有時間把測試包給分一下),happy,回家過元旦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章