【前言】
诸君日安,大魔王要出现啦!接下来要说的是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);
其中 signature
与 publicAddress
会从前端汇入。因为汇入後资料库里面此时应该已经拥有当前登入者的 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
。
验证成功之後就产生一个新的 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();
})
...
那如果失败呢?
最後建设一个 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)
);
};
【小结】
这几天真的有超多新东西,包含Express router
、eth-sig-util
、jsonwebtoken
。光是把程序码里面的每一步看懂都很困难呜呜,但真的很感谢网路上提供各式各样教学资源的大神!
【参考资料】
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
>>: [铁人赛 Day10] Context(下)-花式用法
动词的重点事实上还没讲完 德语难缠的东西就是变化,像是前一篇提到的属格(Genitiv)变化,光是...
这个得上一篇:https://ithelp.ithome.com.tw/articles/10261...
知道了line bot sdk python上的程序的功能是回复你和你传送相同的讯息。这边会看成是在...
引言 今天会介绍解题前的准备工作,以及你需要有什麽样的环境。 解题大致流程 基本上可以归纳出一个解...
现在,基於我们现有的初始对话流与打造完成的语音应用程序。 来试着让它变得更好! 现在我们进入设计对...