程序员社区

Go创建和部署安全的REST API

在本教程中,我们将学习如何使用Go语言开发和部署安全的REST API。

为什么选择Go

Go是一个非常有趣的编程语言,是一种强类型语言,编译非常快,它的性能和c++差不多,Go有goroutine——比线程更高效,并且提供自由的web静态类型——我理解这不是新的功能,但我喜欢go的实现方式。

将创建什么项目呢?

我们将构建一个联系人/电话簿管理应用程序,我们的API将允许用户将联系人添加到他们的个人资料中,他们将能够在他们的手机丢失的情况下找回联系人。

准备工作

假设您已经安装了以下软件包:

  • Go
  • Postgresql
  • Goland IDE

什么是REST?

REST是Representational State Transfer(表述性状态转移)的简称,它是现代客户端应用程序通过http与数据库和服务器通信所使用的机制。因此,如果你有一个新的创业想法,或者你想创建一个很棒的副业项目。REST协议是最好的选择。

创建项目

我们从项目需要使用的go包开始,幸运的是Go标准库已经功能很丰富,无需第三方库就可以实现一个完整的web服务,可以查看net.http包。为了开发的简单,我们将使用以下go包:

  • gorilla/mux:一个强大的URL路由器和分配器。我们使用这个包来匹配URL路由及其处理程序。
  • jinzhu/gorm:Go的orm库,对开发人员非常友好。使用这个orm库可以顺畅的和数据库交互。
  • dgrijalva/jwt-go:用于JWT令牌的签名和验证
  • joho/godotenv:用于将.env文件加载到项目中
    要安装这些包,请打开终端并运行:
go get github.com/{package-name}

这个命令将把软件包安装到您Go环境中。

项目结构:

Go创建和部署安全的REST API插图

utils.go

package utils

import (
    "encoding/json"
    "net/http"
)

func Message(status bool, message string) (map[string]interface{}) {
    return map[string]interface{} {"status" : status, "message" : message}
}

func Respond(w http.ResponseWriter, data map[string] interface{})  {
    w.Header().Add("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

utils.go包含方便的utils函数来构建json消息并返回json响应。在我们继续之前,请注意两个函数Message()和response()。

关于JWT

JWT是一种开放的行业标准RFC 7519方法,用于客户端和服务端安全交互。通过会话很容易识别web应用程序用户,但是,当你的web应用程序API与Android或IOS客户端交互时,由于http请求的无状态特性,会话变得不可用。使用JWT,我们可以为每个经过身份验证的用户创建一个唯一的令牌,这个令牌将包含在向API服务器发出的后续请求头中,这个方法能识别调用我们API的每个用户。看下实现方式:

package app

import (
    "net/http"
    u "lens/utils"
    "strings"
    "go-contacts/models"
    jwt "github.com/dgrijalva/jwt-go"
    "os"
    "context"
    "fmt"
)

var JwtAuthentication = func(next http.Handler) http.Handler {

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        notAuth := []string{"/api/user/new", "/api/user/login"} //不需要认证的请求路径列表
        requestPath := r.URL.Path //当前请求路径

        //检查是否需要认证,如果不需要直接响应请求
        for _, value := range notAuth {

            if value == requestPath {
                next.ServeHTTP(w, r)
                return
            }
        }

        response := make(map[string] interface{})
        tokenHeader := r.Header.Get("Authorization") //从请求头获取token

        if tokenHeader == "" { //令牌不存在,返回错误代码403未授权
            response = u.Message(false, "Missing auth token")
            w.WriteHeader(http.StatusForbidden)
            w.Header().Add("Content-Type", "application/json")
            u.Respond(w, response)
            return
        }

        splitted := strings.Split(tokenHeader, " ") //令牌通常以`Bearer {token-body}`格式出现, 检查检索到的令牌是否符合要求
        if len(splitted) != 2 {
            response = u.Message(false, "Invalid/Malformed auth token")
            w.WriteHeader(http.StatusForbidden)
            w.Header().Add("Content-Type", "application/json")
            u.Respond(w, response)
            return
        }

        tokenPart := splitted[1] //获取token有用部分
        tk := &models.Token{}

        token, err := jwt.ParseWithClaims(tokenPart, tk, func(token *jwt.Token) (interface{}, error) {
            return []byte(os.Getenv("token_password")), nil
        })

        if err != nil { //格式错误的令牌,返回http代码403
            response = u.Message(false, "Malformed authentication token")
            w.WriteHeader(http.StatusForbidden)
            w.Header().Add("Content-Type", "application/json")
            u.Respond(w, response)
            return
        }

        if !token.Valid { //令牌无效,可能未在此服务器上签名
            response = u.Message(false, "Token is not valid.")
            w.WriteHeader(http.StatusForbidden)
            w.Header().Add("Content-Type", "application/json")
            u.Respond(w, response)
            return
        }

        //一切顺利,继续请求并将调用者设置为从已解析的令牌检索到的用户
        fmt.Sprintf("User %", tk.Username) //Useful for monitoring
        ctx := context.WithValue(r.Context(), "user", tk.UserId)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r) //proceed in the middleware chain!
    });
}

通过注释可以清楚的理解上面的代码。创建一个中间件来解析请求,检查当前认证token,检验token是否有效。如果token认证失败,发送错误信息给客户端。验证成功就继续对请求进行处理,接下来会看到如何根据API请求拿到用户信息。

创建用户注册和登录系统

我们希望用户在备份或存储联系方式之前,能够注册和登录系统。我们需要做的第一件事是连接到数据库,我们使用.env文件来存储数据库证书,.env内容如下:

db_name = gocontacts
db_pass = **** //当前用户密码
db_user = postgres
db_type = postgres
db_host = localhost
db_port = 5434
token_password = thisIsTheJwtSecretPassword //不要提交到git

然后,我们可以使用以下代码片段连接到数据库:


package models

import (
    _ "github.com/jinzhu/gorm/dialects/postgres"
    "github.com/jinzhu/gorm"
    "os"
    "github.com/joho/godotenv"
    "fmt"
)

var db *gorm.DB //database

func init() {

    e := godotenv.Load() //Load .env file
    if e != nil {
        fmt.Print(e)
    }

    username := os.Getenv("db_user")
    password := os.Getenv("db_pass")
    dbName := os.Getenv("db_name")
    dbHost := os.Getenv("db_host")


    dbUri := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password) //Build connection string
    fmt.Println(dbUri)

    conn, err := gorm.Open("postgres", dbUri)
    if err != nil {
        fmt.Print(err)
    }

    db = conn
    db.Debug().AutoMigrate(&Account{}, &Contact{}) //Database migration
}

//returns a handle to the DB object
func GetDB() *gorm.DB {
    return db
}

以上代码做了很简单的事情,init函数会在包引入后调用,根据.env文件拿到连接数据库的信息,然后创建一个连接字符串创建数据库连接。

创建应用程序访问端点

到目前为止,我们已经能够创建JWT中间件并连接到我们的数据库。接下来是创建应用程序的入口点,请参阅下面的代码片段:

package main

import (
    "github.com/gorilla/mux"
    "go-contacts/app"
    "os"
    "fmt"
    "net/http"
)

func main() {

    router := mux.NewRouter()
    router.Use(app.JwtAuthentication) //attach JWT auth middleware

    port := os.Getenv("PORT") //Get port from .env file, we did not specify any port so this should return an empty string when tested locally
    if port == "" {
        port = "8000" //localhost
    }

    fmt.Println(port)

    err := http.ListenAndServe(":" + port, router) //Launch the app, visit localhost:8000/api
    if err != nil {
        fmt.Print(err)
    }
}

创建了一个Router对象,使用Use()函数将JWT认证中间件附加到路由器上,然后监听请求。
下面使用位于func main()左边的run按钮来编译和启动应用程序,如果一切正常,你应该在控制台中看不到错误,如果有错误,再看看你的数据库连接参数,看看它们是否相关。

Go创建和部署安全的REST API插图1

创建并认证用户

创建models/account.go文件:


package models

import (
    "github.com/dgrijalva/jwt-go"
    u "lens/utils"
    "strings"
    "github.com/jinzhu/gorm"
    "os"
    "golang.org/x/crypto/bcrypt"
)

/*
JWT claims struct
*/
type Token struct {
    UserId uint
    jwt.StandardClaims
}

//表示用户账号的结构体
type Account struct {
    gorm.Model
    Email string `json:"email"`
    Password string `json:"password"`
    Token string `json:"token";sql:"-"`
}

//校验用户信息
func (account *Account) Validate() (map[string] interface{}, bool) {

    if !strings.Contains(account.Email, "@") {
        return u.Message(false, "Email address is required"), false
    }

    if len(account.Password) < 6 {
        return u.Message(false, "Password is required"), false
    }

    //Email必须唯一
    temp := &Account{}

    //检查错误和重复的电子邮件
    err := GetDB().Table("accounts").Where("email = ?", account.Email).First(temp).Error
    if err != nil && err != gorm.ErrRecordNotFound {
        return u.Message(false, "Connection error. Please retry"), false
    }
    if temp.Email != "" {
        return u.Message(false, "Email address already in use by another user."), false
    }

    return u.Message(false, "Requirement passed"), true
}

func (account *Account) Create() (map[string] interface{}) {

    if resp, ok := account.Validate(); !ok {
        return resp
    }

    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
    account.Password = string(hashedPassword)

    GetDB().Create(account)

    if account.ID <= 0 {
        return u.Message(false, "Failed to create account, connection error.")
    }

    //为新注册的账号创建新的JWT token
    tk := &Token{UserId: account.ID}
    token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
    tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
    account.Token = tokenString

    account.Password = "" //删除密码

    response := u.Message(true, "Account has been created")
    response["account"] = account
    return response
}

func Login(email, password string) (map[string]interface{}) {

    account := &Account{}
    err := GetDB().Table("accounts").Where("email = ?", email).First(account).Error
    if err != nil {
        if err == gorm.ErrRecordNotFound {
            return u.Message(false, "Email address not found")
        }
        return u.Message(false, "Connection error. Please retry")
    }

    err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
    if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { //密码不匹配
        return u.Message(false, "Invalid login credentials. Please try again")
    }
    //登录成功
    account.Password = ""

    //创建 JWT token
    tk := &Token{UserId: account.ID}
    token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
    tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
    account.Token = tokenString //在响应中存放token

    resp := u.Message(true, "Logged In")
    resp["account"] = account
    return resp
}

func GetUser(u uint) *Account {

    acc := &Account{}
    GetDB().Table("accounts").Where("id = ?", u).First(acc)
    if acc.Email == "" { //用户没找到
        return nil
    }

    acc.Password = ""
    return acc
}

account.go实现了一些小功能,我们分解下。第一部分创建了两个结构体Token和Account代表JWT的token声明和用户账号声明。Validate函数检验客户端发送来的数据,Create()函数创建新账号并生成需要返回给客户端的JWT token。Login(username, password)认证已经存在的用户,然后如果认证成功生成JWT token。

authController.go


package controllers

import (
    "net/http"
    u "go-contacts/utils"
    "go-contacts/models"
    "encoding/json"
)

var CreateAccount = func(w http.ResponseWriter, r *http.Request) {

    account := &models.Account{}
    err := json.NewDecoder(r.Body).Decode(account) //decode the request body into struct and failed if any error occur
    if err != nil {
        u.Respond(w, u.Message(false, "Invalid request"))
        return
    }

    resp := account.Create() //Create account
    u.Respond(w, resp)
}

var Authenticate = func(w http.ResponseWriter, r *http.Request) {

    account := &models.Account{}
    err := json.NewDecoder(r.Body).Decode(account) //decode the request body into struct and failed if any error occur
    if err != nil {
        u.Respond(w, u.Message(false, "Invalid request"))
        return
    }

    resp := models.Login(account.Email, account.Password)
    u.Respond(w, resp)
}

代码很简单。包含/user/new和/user/login请求路径的处理函数。将以下代码添加到main函数,注册路由:

router.HandleFunc("/api/user/new", controllers.CreateAccount).Methods("POST")
router.HandleFunc("/api/user/login", controllers.Authenticate).Methods("POST")

上面的代码注册/user/new和/user/login端点并传递它们相应的请求处理程序。
现在,重新编译代码并使用postman访问localhost:8000/api/user/new,将请求体设置为application/json,如下所示:

Go创建和部署安全的REST API插图2

如果您尝试用相同的请求体调用/user/new两次,根据我们的功能实现,您将收到电子邮件已经存在的响应。

创建联系人

应用程序的部分功能是让用户创建/存储联系人。联系人将有name和phone,我们将这些定义为struct的属性。下面的代码片段属于models/contact.go


package models

import (
    u "go-contacts/utils"
    "github.com/jinzhu/gorm"
    "fmt"
)

type Contact struct {
    gorm.Model
    Name string `json:"name"`
    Phone string `json:"phone"`
    UserId uint `json:"user_id"` //该联系人所属的用户
}

/*
检验请求参数是否合法,如果合法返回true
*/
func (contact *Contact) Validate() (map[string] interface{}, bool) {

    if contact.Name == "" {
        return u.Message(false, "Contact name should be on the payload"), false
    }

    if contact.Phone == "" {
        return u.Message(false, "Phone number should be on the payload"), false
    }

    if contact.UserId <= 0 {
        return u.Message(false, "User is not recognized"), false
    }

    //所有需要的参数都已经存在
    return u.Message(true, "success"), true
}

func (contact *Contact) Create() (map[string] interface{}) {

    if resp, ok := contact.Validate(); !ok {
        return resp
    }

    GetDB().Create(contact)

    resp := u.Message(true, "success")
    resp["contact"] = contact
    return resp
}

func GetContact(id uint) (*Contact) {

    contact := &Contact{}
    err := GetDB().Table("contacts").Where("id = ?", id).First(contact).Error
    if err != nil {
        return nil
    }
    return contact
}

func GetContacts(user uint) ([]*Contact) {

    contacts := make([]*Contact, 0)
    err := GetDB().Table("contacts").Where("user_id = ?", user).Find(&contacts).Error
    if err != nil {
        fmt.Println(err)
        return nil
    }

    return contacts
}

与models/Account.go相同。我们创建一个函数Validate()来验证传入的参数,如果发生任何异常,则返回一个错误信息,然后编写函数create()来将这个联系人插入数据库。
剩下的就是检索联系人了。

router.HandleFunc("/api/me/contacts", controllers.GetContactsFor).Methods("GET")

将上面的代码片段添加到main函数。去告诉路由器注册/me/contacts路径。创建controllers.GetContactsFor处理程序处理API请求。

contactsController.go


package controllers

import (
    "net/http"
    "go-contacts/models"
    "encoding/json"
    u "go-contacts/utils"
    "strconv"
    "github.com/gorilla/mux"
    "fmt"
)

var CreateContact = func(w http.ResponseWriter, r *http.Request) {

    user := r.Context().Value("user") . (uint) //获取发送请求的用户的id
    contact := &models.Contact{}

    err := json.NewDecoder(r.Body).Decode(contact)
    if err != nil {
        u.Respond(w, u.Message(false, "Error while decoding request body"))
        return
    }

    contact.UserId = user
    resp := contact.Create()
    u.Respond(w, resp)
}

var GetContactsFor = func(w http.ResponseWriter, r *http.Request) {

    params := mux.Vars(r)
    id, err := strconv.Atoi(params["id"])
    if err != nil {
        //传递的路径参数不是整数
        u.Respond(w, u.Message(false, "There was an error in your request"))
        return
    }
    
    data := models.GetContacts(uint(id))
    resp := u.Message(true, "success")
    resp["data"] = data
    u.Respond(w, resp)
}

和authController.go功能类似,但是它读取json请求内容并解码到Contract结构体中,如果有错误就立刻返回,没有就将联系人信息插入数据库。

获取属于用户的联系人

现在用户可以存储联系人了,万一手机丢失,想查询已经存储的联系人怎么办呢?访问/me/contacts会返回json格式的联系人信息。正常情况下查询用户联系人路径应该是:/user/{userId}/contacts这样的,指定userId作为路径参数是很危险的,因为每个认证过的用户都可以发起这样的请求,将获取到别的用户联系人信息。这可能会导致黑客攻击,这里我将指出JWT到作用。

我们可以使用r.Context().Value("user")很容易的拿到API调用者的id,记住我们将这个id设置到auth.go:我们的认证中间件。

package controllers

import (
    "net/http"
    "go-contacts/models"
    "encoding/json"
    u "go-contacts/utils"
    "strconv"
    "github.com/gorilla/mux"
    "fmt"
)

var CreateContact = func(w http.ResponseWriter, r *http.Request) {

    user := r.Context().Value("user") . (uint) //获取发送请求的用户的id
    contact := &models.Contact{}

    err := json.NewDecoder(r.Body).Decode(contact)
    if err != nil {
        u.Respond(w, u.Message(false, "Error while decoding request body"))
        return
    }

    contact.UserId = user
    resp := contact.Create()
    u.Respond(w, resp)
}

var GetContactsFor = func(w http.ResponseWriter, r *http.Request) {

    params := mux.Vars(r)
    id, err := strconv.Atoi(params["id"])
    if err != nil {
        //传递的路径参数不是整数
        u.Respond(w, u.Message(false, "There was an error in your request"))
        return
    }
    
    data := models.GetContacts(uint(id))
    resp := u.Message(true, "success")
    resp["data"] = data
    u.Respond(w, resp)
}
Go创建和部署安全的REST API插图3
请求/me/contacts的响应

这个项目的代码在github - https://github.com/adigunhammedolalekan/go-contacts上。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go创建和部署安全的REST API

一个分享Java & Python知识的社区