iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
1

account

進入到user的註冊與登錄,因為我們已經把所有的路徑都開給blog用了,account如果要使用其他api路徑有兩種方法

  1. 用if判斷路徑是否是在/account之下,如local.com/account/signup
  2. 使用subdomain,如account.local.com/signup

使用第2種方法需要開其他router同時運行,要讓request對應到不同的router處理一樣有兩種方法

  1. 字串比對
  2. 監聽不同的port

我這裡使用第2種方法,之後靠nginx來導引到正確的port,如果不使用代理工具就只能用第一種方法了,作法在第二天的文章可以看到

先寫設定,打開config/app/app.yaml寫入

servers:
  main: &main_server
    host: your_host
    port: 8000
    RunMode: debug
    ReadTimeout: 60s
    WriteTimeout: 60s
    FilePath: your_file_path # path of user file
    LogPath: your_log_path # path of log file
  account:
    <<: *main_server
    port: 8001

解釋:

  • &main_server註冊一個名為main_server的錨點,可以繼承main以下的資料
  • <<: *main_server引用main_server,下面的port代表只改port,其他port與main相同

改寫router/host_switch.go的HostSwitch為

type HostSwitch struct {
	*gin.Engine
}

// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Check if a http.Handler is registered for the given host.
	// If yes, use it to handle the request.
	// clean path
	path := cleanPath(r.URL.Path)

	// update to request
	r2 := new(http.Request)
	*r2 = *r
	r2.URL = new(url.URL)
	*r2.URL = *r.URL
	r2.URL.Path = path
	hs.Engine.ServeHTTP(w, r2)
}

改寫main.go為

package main

import (
	"app/router"
	"app/setting"
	"log"
	"net/http"

	"fmt"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func main() {
	runServe("main", router.HostSwitch{Engine: router.MainRouter()})
	runServe("account", router.HostSwitch{Engine: router.AccountRouter()})

	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

func runServe(serve string, hs router.HostSwitch)  {
	s := &http.Server{
		Addr:         fmt.Sprintf(":%d", setting.Servers[serve].Port),
		Handler:      hs,
		ReadTimeout:  setting.Servers[serve].ReadTimeout,
		WriteTimeout: setting.Servers[serve].WriteTimeout,
	}
	g.Go(func() error {
		return s.ListenAndServe()
	})
}

解釋:

  • 利用errgroup管理多個goroutine的錯誤訊息,這樣就能同時開兩個router並行運算

來寫router,在router創建account.go寫入

package router

import (
	"app/middleware"
	"app/serve"
	"app/setting"
	"github.com/gin-gonic/gin"
)

func AccountRouter() *gin.Engine {
	r := gin.New()
	gin.SetMode(setting.Servers["account"].RunMode)

	r.Use(middleware.Logging())
	r.Use(middleware.ErrorHandle())

	r.LoadHTMLGlob("view/html/*/*")

	r.PUT("/signup", serve.PutUser)

	return r
}

解釋:

  • 這裡偷懶把create與update寫在一起

要存使用者的帳號密碼不能直接存明文,不然資料庫外洩使用的丈密碼上就被看光光了,一般都會把密碼hash過後存起來,但是這樣還有個問題,如果駭客先將所有輸入的結果的hash值存起來了,就能反向回推出密碼,這被稱之為彩虹表攻擊,為了避免這狀況我們還要加上salt(一組隨機值)來擾亂輸入值。

現在計算的速度越來越快,上述的方法在brute force的攻擊下還是很危險,所以學術界有發展出一套密碼用的hash方法,有名的有bcrypt、scrypt與現在最新的argon2,主要的概念與一般sha等hash背道而馳,密碼上的hash運算速度必須要,從使用者的角度來看,登錄多花一秒的時間不會感覺太大,但如果駭客想用暴力破解,他要猜出一組密碼就必須要多跑到上萬秒以上,再來要使用下一代的hash演算法argon2

argon2是2015的Password Hashing Competition冠軍,利用快速填充記憶體空間來防禦利用GPU等進行硬體加速計算,go已經有支持了,不需要自己刻一個。

我們先在util建立random package,創建random.go寫入

package random

import (
	"crypto/rand"
	"encoding/base64"
)

// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly
func GetRandomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	_, err := rand.Read(b)
	// Note that err == nil only if we read len(b) bytes.
	if err != nil {
		return nil, err
	}

	return b, nil
}

// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
func GetRandomString(s int) (string, error) {
	b, err := GetRandomBytes(s)
	return base64.RawURLEncoding.EncodeToString(b), err
}

接著一樣在util底下建立hash package創建hash.go寫入

package hash

import (
	"app/util/random"
	"encoding/base64"
	"golang.org/x/crypto/argon2"
	"strings"
)

// get hash of argon2 for password hash
func NewPWHash(pw string, time, memory uint32, threads uint8, keyLen uint32) ([]byte, []byte, error) {
	salt, err := random.GetRandomBytes(16)
	if err != nil {
		return nil, nil, err
	}

	return argon2.IDKey([]byte(pw), salt, time, memory, threads, keyLen), salt, nil
}

// generate new hash string in base64 url raw encode with salt of argon2 for password hash
func NewPWHashString(pw string, time, memory uint32, threads uint8, keyLen uint32) (string, string, error) {
	hash, salt, err := NewPWHash(pw, time, memory, threads, keyLen)
	if err != nil {
		return "", "", nil
	}

	return base64.RawURLEncoding.EncodeToString(hash), base64.RawURLEncoding.EncodeToString(salt), nil
}

// get hash string in base64 url raw encode of argon2 for password hash
func GetPWHashString(pw, salt string, time, memory uint32, threads uint8, keyLen uint32) (string, error) {
	saltByte, err := base64.RawURLEncoding.DecodeString(salt)
	if err != nil {
		return "", err
	}

	return base64.RawURLEncoding.EncodeToString(argon2.IDKey([]byte(pw), saltByte, time, memory, threads, keyLen)), nil
}

解釋:

  • NewPWHashString建立一組新的hash string,與對應的salt
  • GetPWHashString將salt與密碼逕行hash後回傳結果

在serve package創建account.go寫入

package serve

import (
	"app/apperr"
	"app/database"
	"app/log"
	"app/setting"
	"app/util/hash"
	"github.com/gin-gonic/gin"
	"net/http"
)

// set for hash parameter
var Params = struct {
	memory      uint32
	iterations  uint32
	parallelism uint8
	saltLength  uint32
	keyLength   uint32
}{
	memory:      65536,
	iterations:  10,
	parallelism: 2,
	saltLength:  16,
	keyLength:   32,
}

// insert an user if oid is 0 else update
func PutUser(c *gin.Context) {

	pw, salt, err := hash.NewPWHashString(c.PostForm("password"), Params.iterations, Params.memory, Params.parallelism, Params.keyLength)
	if err != nil {
		log.Error(c, apperr.ErrPermissionDenied, err, 0, "Sorry, something error", "rand function error")
		return
	}

	err = database.PutUser(c.PostForm("uid"), c.PostForm("username"), pw, c.PostForm("email"), salt)
	if err != nil {
		log.Warn(c, apperr.ErrWrongArgument, err, "sorry, something error. try again", "insert new user fail")
		return
	}

	c.Redirect(http.StatusSeeOther, setting.Servers["main"].Host+strconv.Itoa(setting.Servers["main"].Port))
}

在database/account.go寫入

// insert an user if uid is 0 else update
func PutUser(uid, username, password, email, salt string) error {
	return checkAffect(db.Exec("call put_user(?, ?, ?, ?, ?)", uid, username, password, email, salt))
}

再來寫procedure,進到資料庫後輸入

DELIMITER ;;
CREATE PROCEDURE `put_user`(
  userid INT UNSIGNED,
  user_name VARCHAR(50),
  password CHAR(44),
  email varchar(40),
  salt CHAR(22)
)
BEGIN

  IF userid = 0 THEN

    INSERT INTO `user` (`uid`, `username`, `password`, `email`, `salt`) VALUES (userid, user_name, password, email, salt);

  ELSE
    UPDATE `user`
    SET `user`.`password` = password, `user`.`email` = email, `user`.`salt` = salt
    WHERE `user`.`uid` = userid AND `user`.`username` = user_name;
  END IF;

END ;;
DELIMITER ;

總結

使用者能註冊了,明天寫登錄與權限控管

目前的工作環境

.
├── app
│   ├── apperr
│   │   ├── error.go
│   │   └── handle.go
│   ├── common
│   │   └── cookie.go
│   ├── config
│   │   └── app
│   │       ├── app.yaml
│   │       └── error.yaml
│   ├── database
│   │   ├── connect.go
│   │   ├── error.go
│   │   ├── main.go
│   │   └── scheme.go
│   ├── go.mod
│   ├── go.sum
│   ├── log
│   │   ├── logger.go
│   │   └── logging.go
│   ├── main.go
│   ├── middleware
│   │   ├── error.go
│   │   └── log.go
│   ├── router
│   │   ├── account.go
│   │   ├── host_switch.go
│   │   └── main.go
│   ├── serve
│   │   ├── account.go
│   │   ├── main.go
│   │   └── main_test.go
│   ├── setting
│   │   └── setting.go
│   ├── util
│   │   ├── debug
│   │   │   ├── stack.go
│   │   │   └── stack_test.go
│   │   ├── file
│   │   │   └── file.go
│   │   ├── hash
│   │   │   ├── hash.go
│   │   │   └── hash_test.go
│   │   └── random
│   │       └── random.go
│   └── view
│       ├── css
│       ├── html
│       │   ├── component
│       │   │   ├── blogContainer.html
│       │   │   └── blogList.html
│       │   └── meta
│       │       ├── head.html
│       │       └── index.html
│       └── js
├── config
│   └── app
│       ├── app.yaml
│       └── error.yaml
└── database
    └── maindata

上一篇
Day13 Blog API串接
下一篇
Day15 登入
系列文
從coding到上線-打造自己的blog系統31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言