前言:
本文介绍使用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才能访问的接口:
- 未带token访问时
bash
复制代码
curl http://localhost:8005/auth/hello
响应结果,如:
css
复制代码
{"code":401,"message":"cookie token is empty"}
- 带上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"}