使用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')
    }
});

Restlet Client

这里推荐一个测试接口的chrome插件Restlet Client,感觉比postman更好用。

现在我们再把鉴权过程封装成方法,新建一个util文件login-authorization.js,如下:

const jwt = require('jsonwebtoken');
const fs = require('fs');
const path = require('path');

module.exports = async function (ctx) {
    const requestToken = ctx.request.get('Authorization');
    let cert = fs.readFileSync(path.resolve(__dirname, '../config/jwt_pub.pem'));
    try {
        return await jwt.verify(requestToken, cert);
    } catch (e) {
        ctx.throw(401);
    }
};

这样其他地方要获取解析后的jwt直接调用该方法就可以了。

好了,这下我们基本的账号系统和登陆鉴权就做完了,使用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

> full-stacker-server@0.1.0 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,回家过元旦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容