iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 15
0
Modern Web

從coding到上線-打造自己的blog系統系列 第 15

Day15 登入

login

先來寫登錄,我們需要驗證帳號與密碼是否正確,然後回傳user需要的資料(uid與其下的owner)

進入database後寫入

DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `login`(
  user_name VARCHAR(50),
  password CHAR(44)
)
BEGIN

  SELECT `user`.`uid`, `user`.`username`, GROUP_CONCAT(`owner`.`oid` SEPARATOR "  ") AS subOid, GROUP_CONCAT(`owner`.`uniquename` SEPARATOR "  ") AS subOuniquename, GROUP_CONCAT(`owner`.`nickname` SEPARATOR "  ") AS subOnickname, GROUP_CONCAT(`owner`.`description` SEPARATOR "  ") AS subOdescription
  FROM   `user`
  Left JOIN `owner`
    ON `owner`.`uid` = `user`.`uid`
  WHERE  `user`.`username` = user_name AND `user`.`password` = password
  GROUP BY `user`.`uid`;

END ;;
DELIMITER ;

登錄後就要給予權限了,現在登錄驗證都會採用oauth2的流程,不過我們先別寫的太複雜,我們先簡單設個登錄後生成token紀錄在資料庫並回傳前端,之後前端再用token來驗證就好。

先來寫token的table

CREATE TABLE `token` (
  `uid` int unsigned NOT NULL,
  `accesscode` char(44) NOT NULL COMMENT 'account number',
  `createtime` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`accesscode`)
);

其實把token存在redis等暫存資料庫會更好,不過暫時就先這樣。

現在把token存起來

DELIMITER ;;
CREATE PROCEDURE `new_token`(
  userid INT UNSIGNED,
  code CHAR(44)
)
BEGIN
   INSERT INTO `token` (`uid`, `accesscode`) VALUES (userid, code);
END ;;
DELIMITER ;

別忘了我們的密碼是與salt一起hash的,所以要驗密碼還要先拿出salt

DELIMITER ;;
CREATE PROCEDURE `get_user_salt`(
  username  varchar(40)
)
BEGIN

  SELECT `salt`
  FROM   `user`
  WHERE  `user`.`username` = username;

END ;;
DELIMITER ;

現在來寫go,在database/scheme.go補上

type UserOut struct {
	Uid      int    `json:"uid" xorm:"not null pk autoincr INT(11) 'uid'"`
	Username string `json:"username" xorm:"not null comment('account number') VARCHAR(40) 'username'"`
	OwnerSub `xorm:"extends"`
}

type OwnerSub struct {
	Su             string `json:"subOid" xorm:"VARCHAR(512) 'subOid'"`
	SubUniquename  string `json:"subOuniquename" xorm:"VARCHAR(512) 'subOuniquename'"`
	SubNickname    string `json:"subOnickname" xorm:"VARCHAR(512) 'subOnickname'"`
	SubDescription string `json:"subOdescription" xorm:"VARCHAR(512) 'subOdescription'"`
}

在database/account.go寫入

func Login(userName string, password string) (*UserOut, error) {
	userData := &UserOut{}
	has, err := db.SQL("call login(?, ?)", userName, password).Get(userData)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, nil
	}

	return userData, nil
}

func GetSalt(userName string) (string, error) {
	var salt string
	has, err := db.SQL("call get_user_salt(?)", userName).Get(&salt)
	if err != nil {
		return "", err
	} else if !has {
		return "", nil
	}
	return salt, nil
}

在database下創建auth.go寫入

package database

// generate a new access token
func NewAccessToken(uid, code string) error {
	return checkAffect(db.Exec("call new_token(?, ?)", uid, code))
}

在serve串起來吧!在serve/account.go補上

import (
	"net/url"
	"strconv"
)

func Login(c *gin.Context) {
	// get salt
	userName := c.PostForm("username")
	salt, err := database.GetSalt(userName)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error of login")
		return
	} else if salt == "" {
		log.Warn(c, apperr.ErrWrongArgument, err, "wrong username or password")
		return
	}

	// get hash password
	pw, err := hash.GetPWHashString(c.PostForm("password"), salt, Params.iterations, Params.memory, Params.parallelism, Params.keyLength)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "base64 decode error")
		return
	}

	// login
	userData, err := database.Login(userName, pw)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error of login")
		return
	} else if userData == nil {
		log.Warn(c, apperr.ErrWrongArgument, err, "wrong username or password")
		return
	}

	// generate new token
	uid := strconv.Itoa(userData.Uid)
	code, err := newAccessToken(uid)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error of create token")
	}

	// delete uid
	userData.Uid = 0

	// create new cookie
	c.Writer.Header().Add("Set-Cookie", cookie.CreateCookie("AccessToken", []string{"AccessCode", "uid"},
	[]string{code, uid}, 2592000, "/", "."+setting.Servers["main"].Host, http.SameSiteLaxMode,
	true, true))

	c.JSON(http.StatusOK, gin.H{
		"user": userData,
	})
}

// generate a new token and save it
func newAccessToken(uid string) (string, error) {
	code, err := random.GetRandomString(32)
	if err != nil {
		return "", err
	}
	return code, database.NewAccessToken(uid, code)
}

解釋:

  • 我們先從資料庫取出salt,與密碼一起hash後呼叫login取得user的資訊,接著生成token存入資料庫,最後打包成cookie與user資訊回傳
  • 說明一下cookie的參數
    • Domain: 前面加個點代表包含subdomain都適用這cookie
    • SameSite: 詳情可以看這篇,Lax可以在跨站傳遞(subdomain適用)
    • Secure: true的話只有https才能使用這cookie,但是基於本地測試還沒有憑證所以設false
    • HttpOnly: js無法讀取與寫入

註冊到router,在router/account.go補上

r.POST("/login", serve.Login)

總結

我們已經完成登錄了,接下來寫middleware來控管權限。

目前的工作環境

.
├── app
│   ├── apperr
│   │   ├── error.go
│   │   └── handle.go
│   ├── common
│   │   └── cookie.go
│   ├── config
│   │   └── app
│   │       ├── app.yaml
│   │       └── error.yaml
│   ├── database
│   │   ├── auth.go
│   │   ├── 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
│   │   ├── auth.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

上一篇
Day14 帳戶處理
下一篇
Day16 權限管理
系列文
從coding到上線-打造自己的blog系統30

尚未有邦友留言

立即登入留言