如何在Gin框架中使用JWT实现认证机制
原创作者,公众号【程序员读书】,欢迎关注公众号,转载文章请注明出处哦。
什么是JWT
JWT是JSON Web Token的缩写,是一种跨域认证的解决方案。
使用JWT解决的问题
传统用户认证流程一般是这样的:
从上面的图中,我们可以看到,传统的登录认证的实现,依赖客户端浏览器的cookie和服务器的session,这种实现登录的方式有很大的局限性。
对于部署在单台服务器的应用来说,使用cookie+session登录认证的方案尚且可以接受。
但如果应用程序需要部署到多台服务器上呢?这里面就涉及到session的共享问题,另外,如果不同的域名想实现单点登录功能呢?显示cookie+session同样无法做到。
而要解决上面提出的问题,可以使用JWT,让应用变成无状态,避免session共享问题,而且可以很容易实现服务器的扩展。
JWT的格式
一个正确的JWT格式如下所示:
eyJhbGciOiJIUzI1NiIsInR5c.eyJ1c2VybmFtZaYjiJ9._eCVNYFYnMXwpgGX9Iu412EQSOFuEGl2c
我们看到一个JWT字符串由Header,Payload,Signature三个部分组成,中间使用逗号连接。
Header
Header是一个JSON对象,由token类型和加密算法两个部分组成的,如:
{
"typ": "JWT",//默认为JWT
"alg": "HS256"//支持多种加密算法
}
将上面的JSON对象使用Base64URL算法转换成字符串,即可得到JWT中的Header部分。
注意:JWT编码并不使用Base64,而Base64Url,这是因为Base64生成字符串里,可能会有+,/和=这三个URL中特殊的符号,而我们又可能将token放在URL上传递到服务器上(如test.com?token=xxx), 而Base64URL算法,则是在Base64算法生成的字符串基础上,将=省略,将+替换成-,将/替换成_。
Payload
JWT的Payload部分与Header一样,也是一个JSON对象,用来存放我们实际需要的数据,JWT标准提供了七个可选的字段,分别为:
标题 | 描述 |
---|---|
iss(issuer) | 签发者,其值为大小写敏感的字符串或Uri |
sub(subject) | 主题,用于鉴别一个用户 |
exp(expiration time) | 过期时间 |
aud(audience) | 受众 |
iat(issued at) | 签发时间 |
nbf(not before) | 生效时间 |
jti(JWT ID) | 编号 |
除了标准的字段外,我们可以任意定义私有的字段以满足业务需求,如:
{
iss:"my",//标准字段
jti:"test",//标准字段
username:"aaa",//自定义字段
"gender":"男",
"avatar":"https://1.jpg"
}
将上面的JSON对象使用Base64URL算法转换成字符串,即可得到JWT中的Payload部分。
Signature
Signature是JWT的签名,生成方式为:将Header与Payload进行Base64URL算法编码后,用逗号链接,再使用密钥(secretKey)和Header中指的加密方式进行加密,最终生成Signature。
JWT的特点
- 最好使用HTTPS协议,防止JWT被盗的可能。
- 除了JWT签发时间到期外,没有其他办法让已经生成的JWT失效,除非服务器端换算法。
- 在JWT不加密的情况下,JWT不应该存储敏感的信息,如果要存放敏感信息,最好再次加密。
- JWT最好设置较短的过期时间,防止被盗用后一直有效,降低损失。
- JWT的Payload也可以存储一些业务信息,这样可以减少数据库的查询。
JWT的使用
服务器签发JWT后,发送给客户端,客户端如果是浏览器的话,可以将其存放在cookie或localStorage中,如果是APP的话,则可以存放在sqlite数据库中。
然后每一次接口请求时都带上JWT,而带上来给服务端的方式,也有很多种,比如query、cookie、header或者body,总之就是一切可以带上数据给服务器的方式都可以,但比较规范的做还是通过header Authorization上传,格式如下:
Authorization: Bearer <token>
在Go项目中使用JWT
接下来我们介绍一下在Go项目中如何生成以及解析JWT,这里我们使用
github.com/golang-jwt/jwt
这个库来帮我们生成或解析JWT。
生成
使用NewWithClaims()方法生成Token对象,再通过Token对象的方法来生成JWT字符串,如:
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字符串,如:
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外,我们也可以使用自定义的结构,不过该结构必须实现下面的接口:
type Claims interface {
Valid() error
}
下面是一个实现自定义数据结构的示例:
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标准中定义的字段,可以这样子:
type CustomerClaims struct {
*jwt.StandardClaims//标准字段
Username string `json:"username"`
Gender string `json:"gender"`
Avatar string `json:"avatar"`
Email string `json:"email"`
}
解析
解析是生成反向操作,我们通过解析一个token来获取其中的Header,Payload,并通过Signature校验数据是否被窜改,下面是具体的实现:
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)
}
在Gin项目中使用JWT
通过上面的例子,再结合Gin框架,其实我们完全可以自己实现在Gin使用JWT的需求,但为了不重复造轮子,我们可以直接使用别人造好的轮子。
在Gin框架中,登录认证一般通过中间件来实现,而github.com/appleboy/gin-jwt
这个库中已经集成github.com/golang-jwt/jwt
的实现,并帮我们定义了对应的中间件和控制器。
下面是一个具体的例子
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
命令发起登录请求,如:
curl http://localhost:8005/login -d "username=admin&password=admin"
响应结果,返回token,如:
{"code":200,"expire":"2021-12-16T17:33:39+08:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI"}
请求需要token才能访问的接口:
- 未带token访问时
curl http://localhost:8005/auth/hello
响应结果,如:
{"code":401,"message":"cookie token is empty"}
- 带上token访问时
# 为了方便,先将上面获取的token设置为环境变量
export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI
curl -H"Authorization: Bearer ${TOKEN}" http://localhost:8005/auth/hello
响应结果,如:
{"text":"Hello World.","userID":"admin","userName":"admin"}
总结
希望上面的讲解与例子,能帮助你学习或加深对JWT的理解以及在Go项目中的使用。
当然,除了上面介绍的两个库可以帮我们实现JWT的生成解析外,还有很多的其他的库也能帮我们做到,如果你碰到更好的实现JWT生成与解析的库,欢迎在评论中留言分享!
参考文章
转载自:https://juejin.cn/post/7042520107976753165