Day 13【连动 MetaMask - Back-End Services】这显然是厂商的疏失

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/29c81562-03a2-4308-86fc-273390239ea8/d0c0ebb6433fcf9aedcd48e98080e571.png

【前言】
诸君日安,大魔王要出现啦!接下来要说的是Nonce 的使用、前後端连动,以及帐户验证。今天的内容大部份都参考来自 Amaury Martiny 的 One-click Login with Blockchain: A MetaMask Tutorial!欸不过居然会提前用到 express,我根本不知道要学,这显然是厂商的疏失。

【Back-End Services - User
首先使用 express.Router() 来定义路由,这样未来在前端就可以让这个路径被赋予不同的方法。

import * as controller from './controller';

export const userRouter = express.Router();

/** GET /api/users */
userRouter.route('/').get(controller.find);

/** GET /api/users/:userId */
/** Authenticated route */
userRouter.route('/:userId').get(jwt(config), controller.get);

/** POST /api/users */
userRouter.route('/').post(controller.create);

/** PATCH /api/users/:userId */
/** Authenticated route */
userRouter.route('/:userId').patch(jwt(config), controller.patch);

接下来一一介绍路径内部的方法:

首先是基本宣告,并且要求使用者的 publicAddress 的过程。

import { NextFunction, Request, Response } from 'express';

import { User } from '../../models/user.model';

export const find = (req: Request, res: Response, next: NextFunction) => {
	// If a query string ?publicAddress=... is given, then filter results
	const whereClause =
		req.query && req.query.publicAddress
			? {
					where: { publicAddress: req.query.publicAddress },
			  }
			: undefined;

	return User.findAll(whereClause)
		.then((users: User[]) => res.json(users))
		.catch(next);
};

...

在资料库里面查找 User

...

export const get = (req: Request, res: Response, next: NextFunction) => {
	// AccessToken payload is in req.user.payload, especially its `id` field
	// UserId is the param in /users/:userId
	// We only allow user accessing herself, i.e. require payload.id==userId
	if ((req as any).user.payload.id !== +req.params.userId) {
		return res
			.status(401)
			.send({ error: 'You can can only access yourself' });
	}
	return User.findByPk(req.params.userId) // 查找指定主键的单一活动记录。
		.then((user: User | null) => res.json(user))
		.catch(next);
};

...

在没有登入过的情况时,新建一个使用者。

...

export const create = (req: Request, res: Response, next: NextFunction) =>
	User.create(req.body)
		.then((user: User) => res.json(user))
		.catch(next);

...

【Back-End Services - Auth
Auth 的服务中一样使用 express.Router() 来定义路由。

import express from 'express';

import * as controller from './controller';

export const authRouter = express.Router();

/** POST /api/auth */
authRouter.route('/').post(controller.create);

其中 signaturepublicAddress 会从前端汇入。因为汇入後资料库里面此时应该已经拥有当前登入者的 publicAddress ,因此要找出来。如果找不到就要回报错误,表示没有接收到资料。

import { recoverPersonalSignature } from 'eth-sig-util';
import { bufferToHex } from 'ethereumjs-util';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';

import { config } from '../../config';
import { User } from '../../models/user.model';

export const create = (req: Request, res: Response, next: NextFunction) => {
	const { signature, publicAddress } = req.body;
	if (!signature || !publicAddress)
		return res
			.status(400)
			.send({ error: 'Request should have signature and publicAddress' });

	return (
		User.findOne({ where: { publicAddress } })
			.then((user: User | null) => {
				if (!user) {
					res.status(401).send({
						error: `User with publicAddress ${publicAddress} is not found in database`,
					});

					return null;
				}

				return user;
			})
			...
	);
};

找到使用者之後,宣告要释出 sign-in Message,也就是登入的 nonce 是多少,或想说的话,并且作验证,验证的方法是察看当前登入地址与 signature 解读之後的 publicAddress 是否相符。

	
			...
			.then((user: User | null) => {
				if (!(user instanceof User)) {
					// Should not happen, we should have already sent the response
					throw new Error(
						'User is not defined in "Verify digital signature".'
					);
				}

				const msg = `I am signing my one-time nonce: ${user.nonce}`;

				// We now are in possession of msg, publicAddress and signature. We
				// will use a helper from eth-sig-util to extract the address from the signature
				const msgBufferHex = bufferToHex(Buffer.from(msg, 'utf8'));
				const address = recoverPersonalSignature({
					data: msgBufferHex,
					sig: signature,
				});

				// The signature verification is successful if the address found with
				// sigUtil.recoverPersonalSignature matches the initial publicAddress
				if (address.toLowerCase() === publicAddress.toLowerCase()) {
					return user;
				} else {
					res.status(401).send({
						error: 'Signature verification failed',
					});

					return null;
				}
			})
			...

比较需要注意的是这边使用到了 eth-sig-util 中的 recoverPersonalSignature 来解读 signature

eth-sig-util

验证成功之後就产生一个新的 nonce

	
			...
			.then((user: User | null) => {
				if (!(user instanceof User)) {
					// Should not happen, we should have already sent the response

					throw new Error(
						'User is not defined in "Generate a new nonce for the user".'
					);
				}

				user.nonce = Math.floor(Math.random() * 10000);
				return user.save();
			})
			...

那如果失败呢?

thumb_your-old-password-cant-be-your-new-password-69045654.png

最後建设一个 JWT 来做数位签章之中的授权,使每一次向服务器端送出的需求都是独立的。其中 config 这个物件的 algorithms: ['HS256' as const], secret: 'shhhh'

	
			...
			.then((user: User) => {
				return new Promise<string>((resolve, reject) =>
					// https://github.com/auth0/node-jsonwebtoken
					jwt.sign(
						{
							payload: {
								id: user.id,
								publicAddress,
							},
						},
						config.secret,
						{
							algorithm: config.algorithms[0],
						},
						(err, token) => {
							if (err) {
								return reject(err);
							}
							if (!token) {
								return new Error('Empty token');
							}
							return resolve(token);
						}
					)
				);
			})
			.then((accessToken: string) => res.json({ accessToken }))
			.catch(next)
	);
};

jsonwebtoken

【小结】
这几天真的有超多新东西,包含Express routereth-sig-utiljsonwebtoken。光是把程序码里面的每一步看懂都很困难呜呜,但真的很感谢网路上提供各式各样教学资源的大神!

T7iqjjGh.jpg

【参考资料】
One-click Login with Blockchain: A MetaMask Tutorial
How do I recover the address from message and signature generated with web3.personal.sign?
Web3.js : eth.sign() vs eth.accounts.sign() -- producing different signatures?
Express.js 4.0 的路由(Router)功能用法教学 - G. T. Wang
JWT.IO


<<:  【Day 10】While 回圈

>>:  [铁人赛 Day10] Context(下)-花式用法

Lektion 28. 可拆式动词・动词可以拆 Trennbare Verben

动词的重点事实上还没讲完 德语难缠的东西就是变化,像是前一篇提到的属格(Genitiv)变化,光是...

第58天~

这个得上一篇:https://ithelp.ithome.com.tw/articles/10261...

Day 07 line bot sdk python范例程序在做什麽

知道了line bot sdk python上的程序的功能是回复你和你传送相同的讯息。这边会看成是在...

[2021铁人赛 Day03] 解题前的准备工作

引言 今天会介绍解题前的准备工作,以及你需要有什麽样的环境。 解题大致流程 基本上可以归纳出一个解...

[Day14] 测试与迭代

现在,基於我们现有的初始对话流与打造完成的语音应用程序。 来试着让它变得更好! 现在我们进入设计对...