基於以太坊上實現DApp的登入註冊

Fack發表於2018-05-26

“我總偏向將權利分散於網路。這樣一來,就沒任何組織能輕鬆獲取控制。我不相信巨大的中央組織,天性使然。”——Bob Tayor, ARPANET締造者

本文原創發表於所問HT前端團隊,原文地址:ht.askingdata.com/article/5af…

今年年初的“三點鐘區塊鏈群”徹底激起了以加密數字貨幣的主的區塊鏈浪潮,資本的驅動加深了人們對區塊鏈技術的狂熱。然而,作為區塊鏈3.0的時代的更廣的應用來臨,技術如何落地也是在初期需要解決的第一道坎。

所以今天就來試一下如何在以太坊上建立智慧合約應用(Dapp),開發一個普通應用的該有的登入註冊,以便我們第一時間嚐鮮。

為什麼要使用區塊鏈?

作為一名標準的web開發人員,在開始一門新技術之前,需要仔細考慮一個問題就是:基於現有的業務如果用上區塊鏈會更好嗎?

回答這個問題之前,你需要了解區塊鏈是什麼?優勢是什麼?這個問題就不在這裡展開了,這是個相當大的話題,不清楚的同學可以移步到這裡參考一下。《區塊鏈-百度百科》《區塊鏈技術是什麼?未來可能用於哪些方面?》

那麼區塊鏈本質上就是一個去中心化的資料庫,只不過這個資料庫沒有中心伺服器、資料無法篡改同時一定程度上能夠很好的保護資料隱私。對如今的網際網路來說,聽起來很具有革命性的技術。所以如果對於一款涉及到資料私密性、永久性安全性高的應用,這個確實是非常適合的。

現在在金融、醫療、溯源、社交等等領域,很多公司逐漸開始試水更廣泛的應用。而只靠發幣炒幣,這畢竟是種投機取巧的行為。

以太坊入門必備基礎

接下來將會從零開始搭建基於以太坊web3js專案,開始閱讀之前,你需要熟練前端或後臺JavaScript語法,熟悉區塊鏈思想和原理,如果能瞭解solidity語法更好,因為接下來我們會用到它,和js很像的一門語言。

為了能夠方便大家能夠快速的瞭解,提供了下面幾個資料供參考:

  1. 《ethereum官網》以太坊官網。
  2. 《sails官方文件》一款後臺的nodejs框架。
  3. 《 we3.js 文件1.0版本》以太坊上的前端框架,可實現與合約互動。
  4. 《solidity 文件》以太坊的智慧合約語言,熟悉常用語法,和JavaScript語法類似。

瞭解上面的知識之後,就可以開始DAPP搭建之旅了,將從下面的路線講解:

  1. 搭建以太坊環境。
  2. 建立創世區塊。
  3. 簡單的挖礦、建立賬戶。
  4. 利用以太坊錢包查詢賬戶資訊。
  5. 編寫智慧合約。
  6. web3.js與合約互動。
  7. 登入註冊業務邏輯實現。
  8. postman介面測試

專案程式碼可點選檢視github.com/Elliottssu/…

一、以太坊環境搭建

如果已經有以太坊環境的同學可以跳過,接下來以mac系統為例介紹,windows也差不多。

通過Homebrew來安裝go-ethereum

brew tap ethereum/ethereum

可以新增--devel以下命令來安裝開發分支(建議用這個):

brew install ethereum --devel

執行geth version檢視版本號,如果正常的話即安裝成功。

二、新建創世區塊

在比特幣系統裡,這個創世塊是被寫入原始碼,但對於以太坊而言,創世塊可以是任何你喜歡的東西。你也可以把這個當成是系統的一個漏洞。但是共識演算法確保其它人除非納入你的創世塊,否則是不會有效的。

創世區塊的目的是搭建私有鏈,作為鏈上的第一個塊,如果直接執行節點的話會同步公鏈的資料,資料量會非常大。如果想在同一個網路中獲取資料,創世區塊也必須要一樣。

新建genesis.json檔案內容如下:

{
    "config": {},
    "nonce": "0x0000000000000042",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "difficulty": "0x100",
    "alloc": {},
    "coinbase": "0x0000000000000000000000000000000000000000",
    "timestamp": "0x00",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "extraData": "0x00",
    "gasLimit": "0xffffffffffff"
}
複製程式碼

上面定義了一些如挖礦難度、以太幣數量、gas消耗限制等等資訊。

在當前目錄下執行geth init genesis.json 來初始化創世區塊節點。

至此,環境配置方面已經完成。我們可以通過下面這個命令在8545埠來啟動節點:

geth --rpc --rpccorsdomain "*" --rpcapi "personal,web3,eth,net" console

三、建立賬戶以及挖礦

首先我們需要建立第一個賬戶密碼是12345678,執行命令:

personal.newAccount('12345678')

圖例

建立賬戶之後就可以挖礦了,注意如果有多個帳戶,挖到的以太幣預設都會進入第一個賬戶的餘額裡。

miner.start()啟動挖礦,miner.stop()停止挖礦

圖例

四、利用以太坊錢包查詢賬戶資訊

截止到現在,我們已經成功的啟動以太坊節點,並可以通過命令來新建賬戶,執行挖礦來獲取以太幣操作。可是通過命令我們可能無法直觀的感受在以太坊上賬戶和餘額的變化。

現在通過以太坊官方提供的錢包,來管理賬戶和餘額。下載地址github.com/ethereum/mi…

注意:推薦安裝V0.8.10版本,可以刪除已經部署的合約,方便除錯,最新版的移除掉了改功能。

如果有創世區塊,是私有鏈的話,以太坊錢包會預設開啟私有節點,否則預設同步公鏈上的資料。

圖例

自己可以嘗試用主賬號給其他賬戶轉賬,也可以新建賬號和查詢賬戶餘額。

五、Solidity編寫智慧合約

首先我們需要清楚一個問題,什麼是智慧合約?智慧合約概念可以概括為: 一段程式碼 (智慧合約),執行在可複製、共享的賬本上的計算機程式,可以處理資訊,接收、儲存和傳送價值。通俗的來講就是可以在區塊鏈上執行的程式碼,因為以太坊以前的區塊鏈只能儲存比特幣上的交易資訊,無法做其他事情。而智慧合約的出現,可以在鏈上執行簡單的業務邏輯,這也是區塊鏈應用落地的關鍵。

我們基礎已經準備就緒,接下來就用solidity語言來寫資料的增加和查詢邏輯。

Solidity中合約的含義就是一組程式碼(它的 函式 )和資料(它的 狀態 ),它們位於以太坊區塊鏈的一個特定地址上。 程式碼行 uint time; 宣告一個型別為 uint (256位無符號整數)的狀態變數,叫做 time 。 你可以認為它是資料庫裡的一個位置,可以通過呼叫管理資料庫程式碼的函式進行查詢和變更。對於以太坊來說,上述的合約就是擁有合約(owning contract)。在這種情況下,函式 setget 可以用來變更或取出變數的值。

1. 定義資料結構和變數

這裡只做一個最簡單的賬戶體系,定義個一個使用者的資料結構包含使用者名稱、使用者地址和註冊時間。

定義使用者列表資料結構是為了儲存一個使用者名稱->使用者地址的對映。

//user.sol
//定義使用者資料結構
struct UserStruct {
    address userAddress;
    string username;
    uint time;
    uint index;
}

//定義使用者列表資料結構
struct UserListStruct {
    address userAddress;
    uint index;
}

address[] public userAddresses; //所有地址集合
string[] private usernames; //所有使用者名稱集合
mapping(address => UserStruct) private userStruct; //賬戶個人資訊

mapping(string => UserListStruct) private userListStruct; //使用者名稱對映地址


複製程式碼

address[] private userAddresses; 這一行宣告瞭一個不可以被公開訪問的 address 型別的狀態變數。 address 型別是一個160位的值,且不允許任何算數操作。這種型別適合儲存合約地址或外部人員的金鑰對。如果是關鍵字 public 允許則你在這個合約之外訪問這個狀態變數的當前值。

mapping(address => UserStruct) private userStruct; mapping對映將地址對映到使用者資料結構,這個可以初略理解為一個地址所對應的值有哪些。

2. 判斷使用者名稱或地址是否存在

//user.sol
//判斷使用者地址是否存在
function isExitUserAddress(address _userAddress) public constant returns(bool isIndeed) {
    if (userAddresses.length == 0) return false;
    return (userAddresses[userStruct[_userAddress].index] == _userAddress);
}

//判斷使用者名稱是否存在
function isExitUsername(string _username) public constant returns(bool isIndeed) {
    if (usernames.length == 0) return false;
    return (keccak256(usernames[userListStruct[_username].index]) == keccak256(_username));
}
複製程式碼

這裡我們分別去判斷使用者名稱和地址是否存在,判斷依據是看使用者名稱或地址是否存在於所對應的陣列。

需要注意的是,在JavaScript中判斷一個值是否在陣列中用到的indexOf(),但是在solidity是不支援該函式。有兩種方案:一種是迴圈集合來判斷是否存在,第二種是建立的時候為每條資料加index索引,只需按索引取值。

因為第一種需要遍歷整個陣列,當資料量非常大的時候效率不高,所以通過索引取值的方式更加快速。

3. 新建資料和查詢資料

對於資料的插入和查詢,其實就是往陣列集合中新增和讀取資料。

//user.sol
//根據使用者名稱查詢對於的address
function findUserAddressByUsername(string _username) public constant returns (address userAddress) {
    require(isExitUsername(_username));
    return userListStruct[_username].userAddress;
}


//建立使用者資訊
function createUser(address _userAddress, string _username) public returns (uint index) {
    require(!isExitUserAddress(_userAddress)); //如果地址已存在則不允許再建立

    userAddresses.push(_userAddress); //地址集合push新地址
    userStruct[_userAddress] = UserStruct(_userAddress, _username, now, userAddresses.length - 1);

    usernames.push(_username); //使用者名稱集合push新使用者
    userListStruct[_username] = UserListStruct(_userAddress, usernames.length - 1); //使用者所對應的地址集合

    return userAddresses.length - 1;
}


//獲取使用者個人資訊
function findUser(address _userAddress) public constant returns (address userAddresses, string username, uint time, uint index) {
    require(isExitUserAddress(_userAddress));
    return (
        userStruct[_userAddress].userAddress,
        userStruct[_userAddress].username,
        userStruct[_userAddress].time,
        userStruct[_userAddress].index); 
}
複製程式碼

當然,除了增加和查詢之外,還可對相應的陣列進行修改和刪除。這裡的修改和刪除操作其實並不是真正的更改資料,因為區塊鏈上的資料是無法篡改的。當然除非迫不得已的話,不建議直接在鏈上修改和刪除資料。

六、web3.js與合約互動

現在我們把智慧合約已經寫好了,可以通過js來讀取和新增資料了,但在這之前需要我們部署剛才寫的合約。部署合約有一種比較快捷方便的方法,就是在以太坊錢包裡部署。

圖例
需要注意的是,部署完成後,需要執行挖礦才能成功,因為部署合約(包括寫資料),需要節點通過挖礦來確認交易。

完了之後我們可以在合約列表中找到剛才部署的合約。

tips: 第一次合約部署完成,如果想要推出要執行一次exit,否則合約無法儲存。

這時候可以點進去,執行寫入資料和讀取資料操作了。那麼怎樣才能使用程式碼進行操作呢?

先提前看一下sails檔案目錄:

圖例

1. 安裝truffle

truffle可以將solidity語言的智慧合約,編譯成.json格式的配置檔案,可以用它來和web3.js互動。

全域性安裝truffle,npm install -g truffle

編譯solidity智慧合約,truffle compile

執行之後會在build目錄下輸出編譯後的結果。

2. 拷貝編譯後的檔案中的abi的值

我們編譯的目的是為了拿到abi屬性所對於的配置引數,手動拷貝到,nodejs的配置檔案中。

ps: 這種做法雖然有些傻瓜,但是專案官方推薦的合約部署與讀取要簡單很多。

3. web3.js讀取與建立合約內容

先看看web3.js上是如何呼叫合約的:

讀取methods.myMethod.call,將呼叫“constant”方法並在EVM中執行其智慧合約方法,而不傳送任何事務。注意呼叫不能改變智慧合約狀態;修改methods.myMethod. send,將交易傳送到智慧合約並執行其方法。請注意,這可以改變智慧合約狀態。

那現在就根據以太坊的合約內容,封裝一些web3.js呼叫智慧合約的類。

//Contract.js
const web3Util = require('./Web3Util.js')

class Contract {

    constructor() {
    }

    //user 合約

    /**
     * 判斷使用者名稱是否存在
     */

    static isExitUsername(username, cb) {
        web3Util.contractUser.methods.isExitUsername(username).call()
            .then(result => {
                cb(null, result)
            })
            .catch(err => {
                cb(err.message)
            });
    }

    /**
     * 根據使用者名稱查詢對於的地址
     */
    static findUserAddressByUsername(username, cb) {
        web3Util.contractUser.methods.findUserAddressByUsername(username).call()
            .then(result => {
                cb(null, result)
            })
            .catch(err => {
                cb(err.message)
            });
    }

    /**
     * 查詢使用者資訊
     */
    static findUser(userAddress, cb) {
        web3Util.contractUser.methods.findUser(userAddress).call()
            .then(result => {
                cb(null, result)
            })
            .catch(err => {
                cb(err.message)
            });
    }

    /**
     * 建立使用者資訊 (傳送合約需要先解鎖)
     */
    static createUser(userAddress, username, cb) {
        let options = {
            from: Web3Util.ACCOUNT_ADDRESS_MAIN, //建立賬戶用主賬號
            gas: 10000000 //最大的gas數值
        }
        web3Util.contractUser.methods.createUser(userAddress, username).send(options)
            .then(result => {
                cb(null, result)
            })
            .catch(err => {
                cb(err.message)
            });
    }

}
module.exports = Contract;
複製程式碼

上面的檔案中在Web3Util.js定義了一些公共常量,如合約地址,賬戶地址等等。需要注意的是在使用.send()來建立合約內容的時候要給gas即小費,讀取內容的時候不需要,這個是以太坊智慧合約的必填項,關於gas是如何消耗的大家可以查閱相關資料瞭解。

七、登入註冊業務邏輯實現

截止到目前為止,我們已經成功的將js與solidity連線在一起並且實現互動,那接下來就是實現登入和註冊。

登入其實就是看能否解鎖使用者,然後將使用者的個人資料返回,註冊就是調取智慧合約來寫入一條記錄。

解鎖賬戶(只有解鎖才能執行合約)方法:

//Web3Util.js
/**
 * 解鎖賬戶
 * @param account 賬戶名
 * @param password 密碼
 */
static unlockAccount(account, password, cb) {
    Web3.eth.personal.unlockAccount(account, password, 600)
        .then(result => {
            cb(null, result)
        })
        .catch(err => {
            cb(err.message)
        });
}
複製程式碼

登入註冊執行程式碼:

//AccountController.js

module.exports = {
    //判斷使用者名稱是否存在
    isExitUsername: (req, res) => {
        let username = req.query.username;
        if (!username) return res.json(Message.errMessage('使用者名稱不能為空'));
        Contract.isExitUsername(username, (err, result) => {
            Message.handleResult(res, err, result)
        })
    },

    //登入(使用者名稱或地址登入)
    login: (req, res) => {
        let account = req.body.account
        let password = req.body.password;
        if (!account || !password) return res.json(Message.errMessage('使用者名稱或密碼不能為空'));

        if (Web3.utils.isAddress(account)) { //account is address

            Web3Util.unlockAccount(account, password, (err, result) => {
                if (err) return res.json(Message.errMessage('使用者名稱或密碼錯誤'));
                Contract.findUser(account, (err, result) => {
                    Message.handleResult(res, err, result)
                })
            })
        } else { //account is username

            Contract.findUserAddressByUsername(account, (err, address) => {
                if (err) return res.json(Message.errMessage('使用者名稱或密碼錯誤'));
                Web3Util.unlockAccount(address, password, (err, result) => {
                    if (err) return res.json(Message.errMessage('使用者名稱或密碼錯誤'));
                    Contract.findUser(address, (err, result) => {
                        Message.handleResult(res, err, result)
                    })
                })
            })

        }
    },

    /**
     * 註冊賬戶,在以太坊生成address,使用者名稱會寫在合約中
     */
    register: (req, res) => {
        let username = req.body.username
        let password = req.body.password;
        if (!username || !password) return res.json(Message.errMessage('使用者名稱或密碼不能為空'));
        async.waterfall([
            function (callback) { //檢查使用者名稱是否存在
                Contract.isExitUsername(username, (err, result) => {
                    if (result) return res.json(Message.errMessage('使用者名稱已存在'));
                    callback(null, result)
                })
            },
            function (result, callback) {  //建立使用者 > 生成地址
                Web3.eth.personal.newAccount(password).then(address => {
                    callback(null, address)
                })
            },
            function (address, callback) {  //解鎖主賬戶併合約註冊資訊
                Web3Util.unlockAccount(Web3Util.ACCOUNT_ADDRESS_MAIN, Web3Util.ACCOUNT__PASSWORD_MAIN, (err, result) => {
                    if (err) return res.json(Message.errMessage(err));
                    Contract.createUser(address, username, (err, result) => {
                        if (err) return res.json(Message.errMessage(err));
                        callback(err, result)
                    })
                })
            },
        ], (err, result) => {
            Message.handleResult(res, err, result)
        })
    },

};


複製程式碼

八、postman介面測試

我們已經在router中配置好了路由,接下來使用介面除錯工具來測試一下,這裡使用postman來測試:

注意,開始測試之前需要開啟以太坊節點,保證8545埠開啟:geth --rpc --rpccorsdomain "*" --rpcapi "personal,web3,eth,net" console

因為註冊需要更改合約資料,需要挖礦來確定交易,所以為了方便除錯,順便開啟挖礦:miner.start()

1.註冊賬號

圖例

因為是執行合約交易,註冊完了之後會返回本次交易詳情如塊、消耗的gas等等。如果本次交易失敗,比如再註冊重複的使用者名稱,在solidity中做了攔截,本次交易會失敗,失敗的標誌是返回的gas是自己設定的最大值。

這樣我們就在鏈上建立了一個address,以及這個相對應的使用者名稱和註冊時間資訊。

2.登入賬號(賬號同時支援address和使用者名稱)

圖例

後續

現在以及能夠通過介面與智慧合約互動了,我們可以稍微加個前端頁面,就可以當成一個正常app了,只是資料庫是區塊鏈,是不是很酷。

當然區塊鏈上只能儲存很少的資料,如果要儲存視訊或者圖片,可以藉助IPFS,(是永久的、去中心化儲存和共享檔案的方法,這是一種內容可定址、版本化、點對點超媒體的分散式協議。)配合著區塊鏈能夠實現更加豐富的功能。

目前的缺點在於,讀取和儲存交易資料比較慢,這也是目前Dapp應用無法大規模的開展的一部分原因,但這個並不會阻礙區塊鏈技術的發展,因為它解決的是生產關係,它的思想在於去中心化來防止中央組織的濫用。

在我構思這篇文章的時候,正好是Facebook創始人祖克伯因資料洩漏醜聞在聽證會被輪流質問,利用幾百萬使用者資料來干涉總統大選。使用者隱私資料一旦被攻破或濫用或商業分析推薦,後果也是非常可怕,這也是當今網際網路全球化所帶來的弊端。

所以,如果想要區塊鏈解決這樣的問題還需要多長的路要走?

相關文章