什么是JWT?
JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下。
为什么需要JWT?
在之前一些web项目中,我们通常使用的是cookie-session
模式实现用户认证。相关流程大致如下:
1、用户在浏览器填写用户名和密码,并发送给服务端
2、服务端对用户名和密码校验通过后会生成一份保存当前用户数据信息的session数据和一个与之对用的标识(通常称为sessionid)
3、服务端返回响应将上一步的sessio id写入用户浏览器的cookie
4、后续用户来自浏览器的每次请求都会自动携带包含session id的cookie
5、服务端通过请求中的session_id就能找到之前保存的该用户的session数据,从而获取该用户的相关信息。
这种方案依赖于客户端(浏览器)保存cookie,并且需要在服务端存储用户的session数据。
在移动互联网时代,我们用户可能使用浏览器也可能使用app访问服务,我们web应用可能是前后端分离分开部署不同端口,有时候需要支持第三方登录,cookie-session
的模式有点力不从心了。
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会产生一个JSON对象,经过签名后得到一个Token在发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
生成JWT和解析JWT
我们在这里直接使用jwt-go
这个库来实现我们生成JWT和解析JWT的功能。
定义需求
我们需要定制自己的需求来决定JWT中保存哪些数据,比如我们规定在JWT中要存储username
信息,那么我们定义一个MyClaims
结构体如下:
// MyClaim 自定义声明结构体并内嵌jwt.StandardClaims
// Jwt包自带的jwt.StandardClaim只包含了官方的字段
// 我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
Username string `json:"username"`
jwt.StandardClaim
}
然后我们定义JWT的过期时间,这里以2小时为例:
const TokenExpireDuration = time.Hour * 2
接下来还需要定义Secret:
var MySecret = []byte("夏天夏天悄悄过去")
生成JWT
package main
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
// 定义token过期时间
const (
TokenExpireDuration = time.Hour * 2
)
// 定义secret 用来加盐
var MySecret = []byte("夏天夏天悄悄过去")
// 定义一个结构体
type MyClaim struct {
Username string `json:"username"`
jwt.StandardClaims
}
// GenToken 生成JWT
func GenToken(username string) (string, error) {
// 创建一个我们自己的声明
c := MyClaim{
username, //自定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), //过期时间
Issuer: "little-boy", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整编码后的字符串token
return token.SignedString(MySecret)
}
func main() {
token, err := GenToken("little boy")
if err != nil {
panic(err)
}
fmt.Println(token)
}
// 运行结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpdHRsZSBib3kiLCJleHAiOjE2NTAzNDE0MzYsImlzcyI6ImxpdHRsZS1ib3kifQ.Cm3RdbA6ONJub3U4V4f_H-PE1IXVvniQMlr63ArLpjU
解析JWT
package main
import (
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
// 定义token过期时间
const (
TokenExpireDuration = time.Hour * 2
)
// 定义secret 用来加盐
var MySecret = []byte("夏天夏天悄悄过去")
// 定义一个结构体
type MyClaim struct {
Username string `json:"username"`
jwt.StandardClaims
}
// GenToken 生成JWT
func GenToken(username string) (string, error) {
// 创建一个我们自己的声明
c := MyClaim{
username, //自定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), //过期时间
Issuer: "little-boy", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整编码后的字符串token
return token.SignedString(MySecret)
}
// ParseToken 解析JWT
func ParseToken(tokenstring string) (*MyClaim, error) {
// 解析token
token, err := jwt.ParseWithClaims(tokenstring, &MyClaim{}, func(token *jwt.Token) (interface{}, error) {
return MySecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*MyClaim); ok && token.Valid { //校验token
return claims, nil
}
return nil, errors.New("invalid token")
}
func main() {
// 生成token
token, err := GenToken("little boy")
if err != nil {
panic(err)
}
fmt.Println(token)
//解析token
claims, err := ParseToken(token)
if err != nil {
panic(err)
}
fmt.Printf("解析后的结果: %#v\n", claims)
}
运行结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpdHRsZSBib3kiLCJleHAiOjE2NTAzNDIzNDcsImlzcyI6ImxpdHRsZS1ib3kifQ.3cAki-6Z2mCcxcVRB8G0zT4qkJwzfeWmwT8tIj9ldTM
解析后的结果: &main.MyClaim{Username:"little boy", StandardClaims:jwt.StandardClaims{Audience:"", ExpiresAt:1650342347, Id:"", IssuedAt:0, Issuer:"little-boy", NotBefore:0, Subject:""}}
token模式
JWT(json web token)
分为两部分:
- 生成token
- 对token进行校验
- 判断token是否有效
- 能从token中获取需要的数据
在gin框架中使用JWT
首先我们注册一条路由/auth
,对外提供获取token的渠道:
r.POST("/auth",authHandler)
其中authHandler
定义如下:
// 定义一个结构体,用来保存用户传进来的username和password
type UserInfo struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// 定义一个auth 的处理函数
func authHandler(c *gin.Context) {
// 1、获取请求参数
var user UserInfo
// 2、参数校验
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": "2002",
"msg": "无效的参数",
})
return
}
// 校验用户名和密码是否正确(正常业务场景需于数据库中保存的信息对比)
if user.Username == "littleboy" && user.Password == "root123" {
// 生成token
tokenString, _ := GenToken(user.Username)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"token": tokenString},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 2002,
"msg": "鉴权失败",
})
return
}
func main() {
r := gin.Default()
r.POST("/auth", authHandler)
r.Run(":9999")
}
Postman 运行结果:
用户通过上面的接口获取token之后,后续就会携带token再来请求我们其他接口,这个时候就需要对这些请求的token进行校验了操作了,很显然我们应该实现一个检验token的中间件,具体实现如下:
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 客户端携带token有三种方式 1 放在请求头 2. 放在请求体 3. 放在URI
// 这里假设Token放在Header的Authorization中,并使用Bearer开头
// 具体实现的方式根据你的实际业务决定
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"code": 2003,
"msg": "请求头为空",
})
c.Abort()
return
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(200, gin.H{
"code": 2004,
"msg": "请求头中auth格式有误",
})
c.Abort()
return
}
// parts[1]是获取到的tokenString,使用之前定义好的解析jwt函数来解析他
mc, err := ParseToken(parts[1])
if err != nil {
c.JSON(200, gin.H{
"code": 2005,
"msg": "无效的token",
})
c.Abort()
return
}
// 将当前请求的username信息保存到请求的上下文
c.Set("username", mc.Username)
c.Next() //后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
}
}
注册一个/home的路由:
r.GET("/home", JWTAuthMiddleware(), homeHandler)
// home的handler函数
func homeHandler(c *gin.Context) {
username := c.MustGet("username").(string)
c.JSON(
http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"username": username},
})
}
验证
postman测试:
#先获取token
1、发送POST: http://127.0.0.1:9999/auth
2、加上json: {
"username": "littleboy",
"password":"root123"
}
3、获得返回数据
{
"code": 2000,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpdHRsZWJveSIsImV4cCI6MTY1MDM0NzIwNCwiaXNzIjoibGl0dGxlLWJveSJ9.oUZAlTiUMysl2Gdt-5Z0dzxiMaT_C6RWHPTeMI1TioA"
},
"msg": "success"
}
4、将获得的token加在postman的authorization中的bearer token中
5、发送get: http://127.0.0.1:9999/home
6、得到返回数据
{
"code": 2000,
"data": {
"username": "littleboy"
},
"msg": "success"
}