Go Gin 建立 API - 完成CRUD, Login, JWT Auth
tags: Go
Gin
API
category: Back-End
description: Go Gin 建立 API - 完成CRUD, Login, JWT Auth
created_at: 2021/07/09 16:00:00
寫法偏舊,未來有空會再寫一個新的系列,有興趣再往下閱讀
上一篇最後的檔案在Github
上一篇偷懶沒做完RUD,現在快速把他寫完吧,依照上次的設計
router -> api -> service -> dao -> model
可以先在 api 挖空的 function 出來,之後讓 router 去註冊,再來寫完 model -> dao -> service,再把 api 與 service 組裝起來。
但在 service
之前,要先寫好相應的 request
request
// ...
type LoginUserRequest struct {
Username string `form:"username" binding:"required,max=100"`
Password string `form:"password" binding:"required,max=100"`
}
type GetUserRequest struct {
ID uint `form:"id" binding:"required"`
}
type UpdateUserRequest struct {
ID uint `form:"id" binding:"required"`
Name string `form:"name" binding:"max=100"`
Username string `form:"username" binding:"max=100"`
Password string `form:"password" binding:"max=100"`
}
api
// ...
func (u User) Login(c *gin.Context) {}
func (u User) Get(c *gin.Context) {}
func (u User) Update(c *gin.Context) {}
func (u User) Delete(c *gin.Context) {}
router
// ...
router.POST("/login", user.Login)
router.GET("/users/:id", user.Get)
router.PATCH("/users/:id", user.Update)
router.DELETE("/users/:id", user.Delete)
// ...
model
// ...
func (u User) Update(db *gorm.DB, newUser User) error {
return db.Model(&u).Updates(newUser).Error
}
func (u User) Delete(db *gorm.DB) error {
return db.Unscoped().Delete(&u).Error
}
func (u User) Get(db *gorm.DB) (User, error) {
var user User
var err error
if u.Username != "" {
db = db.Where("username = ?", u.Username)
}
if u.ID != 0 {
err = db.First(&user, u.ID).Error
} else {
err = db.First(&user).Error
}
if err != nil {
return user, err
}
return user, nil
}
dao
// ...
func (dao *Dao) UpdateUser(id uint, newUser model.User) error {
user := model.User{
Model: gorm.Model{
ID: id,
},
}
return user.Update(dao.Engine, newUser)
}
func (dao *Dao) DeleteUser(id uint) error {
user := model.User{
Model: gorm.Model{
ID: id,
},
}
return user.Delete(dao.Engine)
}
func (dao *Dao) GetUserByUsername(username string) (model.User, error) {
user := model.User{
Username: username,
}
return user.Get(dao.Engine)
}
func (dao *Dao) GetUserByID(id uint) (model.User, error) {
user := model.User{
Model: gorm.Model{
ID: id,
},
}
return user.Get(dao.Engine)
}
service
// ...
func (s Service) UpdateUser(id uint, params *requests.UpdateUserRequest) error {
newUser := model.User{}
if params.Name != "" {
newUser.Name = params.Name
}
if params.Username != "" {
newUser.Username = params.Username
}
if params.Password != "" {
newUser.Password, _ = utils.HashPassword(params.Password)
}
return s.Dao.UpdateUser(id, newUser)
}
func (s Service) DeleteUser(id uint) error {
return s.Dao.DeleteUser(id)
}
func (s Service) GetUserById(id uint) (model.User, error) {
return s.Dao.GetUserByID(id)
}
在建立一個 pkg/converts/convert.go
負責處理字串轉數字
package converts
import "strconv"
type StrTo string
func (str StrTo) String() string {
return string(str)
}
func (str StrTo) Int() (int, error){
value, err := strconv.Atoi(str.String())
return value, err
}
func (str StrTo) MustInt() int {
value, _ := str.Int()
return value
}
func (str StrTo) UInt32() (uint32, error) {
value, err := strconv.Atoi(str.String())
return uint32(value), err
}
func (str StrTo) MustUInt32() uint32 {
value, _ := str.UInt32()
return value
}
func (str StrTo) UInt() (uint, error) {
value, err := strconv.Atoi(str.String())
return uint(value), err
}
func (str StrTo) MustUInt() uint {
value, _ := str.UInt()
return value
}
工具都準備齊全之後,可以寫核心 api code 了 api
// ...
func (u User) Get(c *gin.Context) {
id := converts.StrTo(c.Param("id")).MustUInt()
params := requests.GetUserRequest {
ID: id,
}
response := app.NewResponse(c)
valid, err := app.BindAndValidation(c, ¶ms)
if !valid {
errorResponse := errors.InvalidParams.WithDetails(err...)
response.MakeErrorResponse(errorResponse)
return
}
s := service.New(c.Request.Context())
user, userErr := s.GetUserById(id)
if userErr != nil {
response.MakeErrorResponse(errors.NotFound)
return
}
response.MakeResponse(user)
}
func (u User) Update(c *gin.Context) {
id := converts.StrTo(c.Param("id")).MustUInt()
params := requests.UpdateUserRequest {
ID: id,
}
response := app.NewResponse(c)
valid, err := app.BindAndValidation(c, ¶ms)
if !valid {
errorResponse := errors.InvalidParams.WithDetails(err...)
response.MakeErrorResponse(errorResponse)
return
}
s := service.New(c.Request.Context())
updateErr := s.UpdateUser(id, ¶ms)
if updateErr != nil {
response.MakeErrorResponse(errors.UpdateUserFail)
return
}
response.MakeResponse("")
}
func (u User) Delete(c *gin.Context) {
id := converts.StrTo(c.Param("id")).MustUInt()
response := app.NewResponse(c)
s := service.New(c.Request.Context())
err := s.DeleteUser(id)
if err != nil {
response.MakeErrorResponse(errors.DeleteUserFail)
return
}
response.MakeResponse("")
}
// ...
這樣子剩下的 RUD 就完成了,再來做登入取得 JWT Token
在登入之前先安裝 JWT 使用的套件
$ go get github.com/golang-jwt/jwt
先打開 config.yaml
加入 JWT 的相關設定
# ...
JWT:
Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Expire: 3600
- Secret: 放一個只有你知道長字串,加密用的
- Expire: Token 發出來之後的時效(秒數)
設定好之後要在 pkg/setting/setting.go
建立設定的結構
type JWTSetting struct {
Secret string
Expire time.Duration
}
再到 global/setting.go
註冊到全域
global/setting.go
var (
// ...
JWTSetting *setting.JWTSetting
)
之後回到 main.go
激活 global setting
func setupSetting() error {
// ...
err = setting.ReadSection("JWT", &global.JWTSetting)
if err != nil {
return err
}
global.JWTSetting.Expire *= time.Second
// ...
}
再來建立 pkg/app/jwt
檔案,負責生成和解析 Token
(假設我只在 payload
裝 username
與當前時間)
package app
import (
"github.com/LaiJunBin/gin-api/global"
"github.com/golang-jwt/jwt"
"time"
)
type Claims struct {
Username string `json:"username"`
NowTime time.Time `json:"created_at"`
jwt.StandardClaims
}
func GetJWTSecret() []byte {
return []byte(global.JWTSetting.Secret)
}
func GenerateToken(username string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(global.JWTSetting.Expire)
claims := Claims{
Username: username,
NowTime: nowTime,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(GetJWTSecret())
return token, err
}
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return GetJWTSecret(), nil
})
if tokenClaims != nil {
claims, ok := tokenClaims.Claims.(*Claims)
if ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
再來就可以開始做登入的功能了
service 只要增加一個
func (s Service) GetUserByUsername(username string) (model.User, error) {
user, err := s.Dao.GetUserByUsername(username)
return user, err
}
api 的部分則是填滿 Login()
function
func (u User) Login(c *gin.Context) {
params := requests.LoginUserRequest{}
response := app.NewResponse(c)
valid, err := app.BindAndValidation(c, ¶ms)
if !valid {
errorResponse := errors.InvalidParams.WithDetails(err...)
response.MakeErrorResponse(errorResponse)
return
}
s := service.New(c.Request.Context())
user, userErr := s.GetUserByUsername(params.Username)
if userErr != nil {
response.MakeErrorResponse(errors.LoginFail)
return
}
if !utils.CheckPasswordHash(params.Password, user.Password) {
response.MakeErrorResponse(errors.LoginFail)
return
}
token, tokenErr := app.GenerateToken(user.Username)
if tokenErr != nil {
response.MakeErrorResponse(errors.UnauthorizedTokenGenerate)
return
}
response.MakeResponse(token)
}
登入成功會看到以下回應格式,data 就是 token
{
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIwNCIsImNyZWF0ZWRfYXQiOiIyMDIxLTA3LTA5VDIyOjI0OjAyLjA4ODkzMzgrMDg6MDAiLCJleHAiOjE2MjU4NDQyNDJ9.2SR5Jux906-wZbw9sKW55aeGUOTRPW1_u7YeLUBNsag",
"success": true
}
再來為了要解析 token , 還會需要一個中介層(middleware),所以建立一個新檔案 internal/middleware/jwt.go
package middleware
import (
"github.com/LaiJunBin/gin-api/pkg/app"
"github.com/LaiJunBin/gin-api/pkg/errors"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"strings"
)
func JWT() gin.HandlerFunc {
return func(ctx *gin.Context) {
var errResponse *errors.Error
authorization := ctx.GetHeader("Authorization")
splitAuthorization := strings.SplitN(authorization, "Bearer ", 2)
if len(splitAuthorization) != 2{
errResponse = errors.InvalidParams
} else {
token := strings.TrimSpace(splitAuthorization[1])
if token == "" {
errResponse = errors.InvalidParams
} else {
claims, err := app.ParseToken(token)
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorExpired:
errResponse = errors.UnauthorizedTokenTimeout
break
default:
errResponse = errors.UnauthorizedTokenError
break
}
}
ctx.Set("claims", claims)
}
}
if errResponse != nil {
response := app.NewResponse(ctx)
response.MakeErrorResponse(errResponse)
ctx.Abort()
return
}
ctx.Next()
}
}
寫好中介層之後要記得去 Use()
, 要想一下有哪些路由需要經過 JWT 驗證在包進去,所以要稍微整理一下路由。
func NewRouter() *gin.Engine {
router := gin.New()
router.Use(middleware.Translations())
user := api.NewUser()
router.POST("/register", user.Register)
router.POST("/login", user.Login)
router.GET("/users/:id", user.Get)
authRouter := router.Group("/")
authRouter.Use(middleware.JWT())
authRouter.PATCH("/users/:id", user.Update)
authRouter.DELETE("/users/:id", user.Delete)
return router
}
這樣子基本的驗證就完成了
- 不需要 Token
- 註冊
- 登入
- 查User
- 需要 Token
- 更新
- 刪除
簡單的拿刪除User當作範例,首先先在 service 加上一個 function,負責判斷有沒有權限
func (s Service) CheckPermission(id uint, claims *app.Claims) bool {
user, err := s.GetUserById(id)
if err != nil {
return false
}
return user.Username == claims.Username
}
在來把 api
中的 Delete()
函數改一下
func (u User) Delete(c *gin.Context) {
s := service.New(c.Request.Context())
id := converts.StrTo(c.Param("id")).MustUInt()
claims := c.Keys["claims"].(*app.Claims)
response := app.NewResponse(c)
if !s.CheckPermission(id, claims) {
response.MakeErrorResponse(errors.StatusUnauthorized)
return
}
err := s.DeleteUser(id)
if err != nil {
response.MakeErrorResponse(errors.DeleteUserFail)
return
}
response.MakeResponse("")
}
這樣的話只有自己可以刪除自己,如果 token 對不上的話就會噴401
本來這一篇還要寫檔案上傳的,不過好像已經有點長了,還是延到下一篇吧!絕對不是我累了
這篇進度的Source code