如何将我的服务开放给用户:构建 API 接口和用户认证的实践指南| 青训营

72 阅读1分钟

前言:

   本文介绍使用Golang构建API接口和用户认证的方法,内容主要来自青训营教学视频,也有部分自己的理解。文章以易懂为主,如有错误欢迎留言指正。

1.构建API接口

   API,即应用程序接口,可以比喻为小区的门禁服务,它可以限制用户访问,通过API接口,用户还可以请求数据,上传信息,执行操作等。    下文介绍使用Golang构建API接口的方式。

1.1 认识GIN框架

   这里使用Gin框架来构建API接口,Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

   Gin框架是开源的,可以在github上下载其源码库,查看相应的说明。Gin源码库地址:github.com/gin-gonic/g…

1.2 安装GIN框架

1.2.1 环境要求

gin框架需要go语言版本在1.6及以上。可以使用go version查看自己的go语言版本是否符合要求。

1.2.2 安装gin框架库

通过go get命令安装gin框架:

go get -u github.com/gin-gonic/gin

安装完毕后,可以在当前系统的$GOPATH目录下的src/github.com目录中找到gin-gonic目录,该目录下存放的就是gin框架的源码。

1.3 使用GIN创建接口

   使用GIn创建接口十分简单;

    //初始化gin
    eng := gin.Default()

   // 设置一个get请求的路由,url为localhost
   eng.GET("/", func(c *gin.Context) {
       //获取数据
       //获取handler处理
       //返回数据
      c.String(http.StatusOK, "hello World!")
   })
       //监听并启动服务,默认 http://localhost:8080/
   eng.Run()


2.用户认证

   用户认证需要集成jwt。这里我们使用 github.com/golang-jwt/jwt这个库来帮我们生成或解析JWT。

2.1 生成

使用NewWithClaims()方法生成Token对象,再通过Token对象的方法来生成JWT字符串,如:

go
复制代码
package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("111")//密钥,不能泄露
    //生成token对象
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "foo": "bar",
	"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    })
    //生成jwt字符串
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

也可使用New()方法生成Token对象,再生成JWT字符串,如:

go
复制代码
package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("111")
    token := jwt.New(jwt.SigningMethodHS256)
    //通过New方法不能在创建的时候携带数据,因此可以通过给token.Claims赋值来定义数据
    token.Claims = jwt.MapClaims{
	"foo": "bar",
	"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

上面的例子中,是通过jwt.MapClaims这个数据结构定义JWT中的Payload数据的,除了使用jwt.MapClaims外,我们也可以使用自定义的结构,不过该结构必须实现下面的接口:

go
复制代码
type Claims interface {
    Valid() error
}

下面是一个实现自定义数据结构的示例:

go
复制代码
package main

import (
	"fmt"
	"github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}

func (c CustomerClaims) Valid() error {
return nil
}

func main() {
    //密钥
    hmacSampleSecret := []byte("111")
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = CustomerClaims{
        Username: "小明",
	Gender:   "男",
	Avatar:   "https://1.jpg",
	Email:    "test@163.com",
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

如果我们想在自定义结构中使用JWT标准中定义的字段,可以这样子:

go
复制代码
type CustomerClaims struct {
    *jwt.StandardClaims//标准字段
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}

2.2 解析

解析是生成反向操作,我们通过解析一个token来获取其中的Header,Payload,并通过Signature校验数据是否被窜改,下面是具体的实现:

go
复制代码
package main

import (
	"fmt"

	"github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
	Username string `json:"username"`
	Gender   string `json:"gender"`
	Avatar   string `json:"avatar"`
	Email    string `json:"email"`
	jwt.StandardClaims
}

func main() {
	var hmacSampleSecret = []byte("111")
        //前面例子生成的token
	tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuWwj-aYjiIsImdlbmRlciI6IueUtyIsImF2YXRhciI6Imh0dHBzOi8vMS5qcGciLCJlbWFpbCI6InRlc3RAMTYzLmNvbSJ9.mJlWv5lblREwgnP6wWg-P75VC1FqQTs8iOdOzX6Efqk"

	token, err := jwt.ParseWithClaims(tokenString, &CustomerClaims{}, func(t *jwt.Token) (interface{}, error) {
		return hmacSampleSecret, nil
	})

	if err != nil {
		fmt.Println(err)
		return
	}
	claims := token.Claims.(*CustomerClaims)
	fmt.Println(claims)
}

2.3 实战-在Gin项目中使用JWT

通过上面的例子,再结合Gin框架,其实我们完全可以自己实现在Gin使用JWT的需求,但为了不重复造轮子,我们可以直接使用别人造好的轮子。

在Gin框架中,登录认证一般通过中间件来实现,而github.com/appleboy/gin-jwt这个库中已经集成github.com/golang-jwt/jwt的实现,并帮我们定义了对应的中间件和控制器。

下面是一个具体的例子

go
复制代码
package main

import (
    "log"
    "net/http"
    "time"

    jwt "github.com/appleboy/gin-jwt/v2"
    "github.com/gin-gonic/gin"
)

//用于接受登录的用户名与密码
type login struct {
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

var identityKey = "id"

//jwt中payload的数据
type User struct {
    UserName  string
    FirstName string
    LastName  string
}

func main() {

    // 定义一个Gin的中间件
    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
	Realm:            "test zone",          //标识
	SigningAlgorithm: "HS256",              //加密算法
	Key:              []byte("secret key"), //密钥
	Timeout:          time.Hour,
	MaxRefresh:       time.Hour,   //刷新最大延长时间
	IdentityKey:      identityKey, //指定cookie的id
	PayloadFunc: func(data interface{}) jwt.MapClaims { //负载,这里可以定义返回jwt中的payload数据
            if v, ok := data.(*User); ok {
		return jwt.MapClaims{
                    identityKey: v.UserName,
		}
            }
            return jwt.MapClaims{}
	},
	IdentityHandler: func(c *gin.Context) interface{} {
            claims := jwt.ExtractClaims(c)
            return &User{
                UserName: claims[identityKey].(string),
            }
	},
	Authenticator: Authenticator, //在这里可以写我们的登录验证逻辑
	Authorizator: func(data interface{}, c *gin.Context) bool { //当用户通过token请求受限接口时,会经过这段逻辑
            if v, ok := data.(*User); ok && v.UserName == "admin" {
		return true
            }

            return false
	},
	Unauthorized: func(c *gin.Context, code int, message string) { //错误时响应
		c.JSON(code, gin.H{
                    "code":    code,
                    "message": message,
		})
	},
		// 指定从哪里获取token 其格式为:"<source>:<name>" 如有多个,用逗号隔开
	TokenLookup:   "header: Authorization, query: token, cookie: jwt",
	TokenHeadName: "Bearer",
	TimeFunc:      time.Now,
})

    if err != nil {
        log.Fatal("JWT Error:" + err.Error())
    }
    r := gin.Default()
    //登录接口
    r.POST("/login", authMiddleware.LoginHandler)
    auth := r.Group("/auth")
    //退出登录
    auth.POST("/logout", authMiddleware.LogoutHandler)
    // 刷新token,延长token的有效期
    auth.POST("/refresh_token", authMiddleware.RefreshHandler)
    auth.Use(authMiddleware.MiddlewareFunc()) //应用中间件
    {
        auth.GET("/hello", helloHandler)
    }

    if err := http.ListenAndServe(":8005", r); err != nil {
	log.Fatal(err)
    }
}

func Authenticator(c *gin.Context) (interface{}, error) {
	var loginVals login
	if err := c.ShouldBind(&loginVals); err != nil {
		return "", jwt.ErrMissingLoginValues
	}
	userID := loginVals.Username
	password := loginVals.Password

	if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
		return &User{
			UserName:  userID,
			LastName:  "Bo-Yi",
			FirstName: "Wu",
		}, nil
	}

	return nil, jwt.ErrFailedAuthentication
}

//处理/hellow路由的控制器
func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c)
	user, _ := c.Get(identityKey)
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

将服务器运行起来后,通过curl命令发起登录请求,如:

bash
复制代码
curl http://localhost:8005/login -d "username=admin&password=admin"

响应结果,返回token,如:

css
复制代码
{"code":200,"expire":"2021-12-16T17:33:39+08:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI"}

请求需要token才能访问的接口:

  1. 未带token访问时
bash
复制代码
curl http://localhost:8005/auth/hello

响应结果,如:

css
复制代码
{"code":401,"message":"cookie token is empty"}
  1. 带上token访问时
bash
复制代码
# 为了方便,先将上面获取的token设置为环境变量
export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI

curl -H"Authorization: Bearer ${TOKEN}" http://localhost:8005/auth/hello

响应结果,如:

json
复制代码
{"text":"Hello World.","userID":"admin","userName":"admin"}