Using JWT in Go Gin

1. JWT

The full name of JWT is JSON Web Token, which is a cross-domain authentication solution and is an open standard. It specifies a token implementation method and is currently mostly used in front-end and back-end separation projects and OAuth2.0 business scenarios.

2. Why should you use JWT in your Gin?

The traditional Cookie-Sesson mode occupies server memory and has poor scalability. If you encounter cluster or cross-service verification scenarios, you must support Sesson replication or sesson persistence.

1.Basic principles of JWT

After server verification, get user information JSON

1

2

3

4

5

{

     "user_id": "xxxxxx",

    "role": "xxxxxx",

    "refresh_token": "xxxxx"

}

(1) How to form JWT TOKEN

JWT is a very long string

eyJhbGciOiJI123afasrwrqqewrcCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It consists of three parts, each part is separated by a dot (.), and the three parts are as follows

 

  • Header
  • Payload
  • Signature

1)Header

Header is a JSON object encrypted by BASE64URL algorithm. After decryption, it is as follows

1

2

3

4

{

  "alg": "HS256",

  "typ": "JWT"

}

Among them, the alg attribute indicates the algorithm used for the signature, and the default is HS256;

typ represents the type of the current token, and the type of JWT is jwt

Base64URL

Similar to BASE64, since the symbols +, /, = have special meanings in URLs, the BASE64RUL algorithm replaces these symbols.

+ -> -

= -> ignored

/ -> _

2)Payload

The Payload part is also a JSON object converted into a string through the BASE64URL algorithm. The Payload part contains 7 basic fields.

  • iss (issuer): issuer
  • exp (expiration time): expiration time
  • sub (subject):subject
  • aud (audience): audience
  • nbf (Not Before): Effective time
  • iat (Issued At): issuance time
  • jti (JWT ID): number

You can also insert custom business fields into it, as follows

user_id

role

3)Signature

The Signature part is a signature of the first two parts to prevent data tampering .

First, you need to specify a secret. This key is known only to the server and cannot be leaked to users. Then, use the signature algorithm specified in the Header (the default is HMAC SHA256) to generate a signature according to the following formula.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

(2) Decryption process

When the system receives the TOKEN, it takes out the strings of Header and Payload. After splicing them together, it uses the hash method specified in the Header to pass the formula.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Encrypt to obtain ciphertext

Then compare the ciphertext just obtained with the ciphertext passed by TOKEN. If they are equal, it means that the ciphertext has not changed.

3. Some characteristics of JWT (advantages and disadvantages)

  • JWT is not encrypted by default, but it can also be encrypted. After generating the original Token, it can be encrypted again with the key.
  • Secret data cannot be written to a JWT without encryption.
  • JWT can be used not only for authentication but also for exchanging information. Effective use of JWT can reduce the number of times the server queries the database.
  • The biggest disadvantage of JWT is that because the server does not save the session state, it cannot revoke a token or change the permissions of the token during use. That is, once a JWT is issued, it will remain valid until it expires, unless the server deploys additional logic.
  • The JWT itself contains authentication information, and once leaked, anyone can obtain all permissions of the token. To reduce theft, the validity period of JWT should be set relatively short. For some more important permissions, users should be authenticated again when using them.
  • In order to reduce theft, JWT should not be transmitted plainly using the HTTP protocol, but should be transmitted using the HTTPS protocol.

1.GIN integrates JWT

1

2

go get -u github.com/dgrijalva/jwt-go

go get github.com/gin-gonic/gin

Write jwtutil

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

var Secret = []byte("whatasecret")

// jwt过期时间, 按照实际环境设置

const expiration = 2 * time.Minute

type Claims struct {

    // 自定义字段, 可以存在用户名, 用户ID, 用户角色等等

    Username string

    // jwt.StandardClaims包含了官方定义的字段

    jwt.StandardClaims

}

func GenToken(username string) (string, error) {

    // 创建声明

    a := &Claims{

        Username: username,

        StandardClaims: jwt.StandardClaims{

            ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间

            IssuedAt:  time.Now().Unix(),                 // 签发时间

            Issuer:    "gin-jwt-demo",                    // 签发者

            Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效

            NotBefore: 0,                                 // 生效起始时间

            Subject:   "",                                // 主题

        },

    }

    // 用指定的哈希方法创建签名对象

    tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)

    // 用上面的声明和签名对象签名字符串token

    // 1. 先对Header和PayLoad进行Base64URL转换

    // 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起

    // 3. 用secret对拼接在一起之后的字符串进行HASH加密

    // 4. 连在一起返回

    return tt.SignedString(Secret)

}

func ParseToken(tokenStr string) (*Claims, error) {

    // 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断

    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {

        return Secret, nil

    })

    if err != nil {

        return nil, err

    }

    // 校验token

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {

        return claims, nil

    }

    return nil, errors.New("invalid token")

}

  • Secret is the secret key, used for encrypted signatures
  • expiration is the TOKEN expiration time
  • Claims is a signature claim object, including custom fields and fields specified by JWT

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

type Claims struct {

    // 自定义字段, 可以存在用户名, 用户ID, 用户角色等等

    Username string

    // jwt.StandardClaims包含了官方定义的字段

    jwt.StandardClaims

}

type StandardClaims struct {

    Audience  string `json:"aud,omitempty"`

    ExpiresAt int64  `json:"exp,omitempty"`

    Id        string `json:"jti,omitempty"`

    IssuedAt  int64  `json:"iat,omitempty"`

    Issuer    string `json:"iss,omitempty"`

    NotBefore int64  `json:"nbf,omitempty"`

    Subject   string `json:"sub,omitempty"`

}

(1)GenToken method

The GenToken method generates a token for a certain username, which is different every time.

jwt.NewWithClaims(jwt.SigningMethodHS256, a) declares a signature object and specifies the hash algorithm of HS256

token.SignedString(Secret) indicates that the declared object and SECRET are encrypted using the specified hash algorithm, including the following process

  • First perform Base64URL conversion on Header and PayLoad
  • The converted strings of Header and PayLoadBase64URL are spliced ​​together with .
  • Use secret to HASH the strings spliced ​​together
  • BASE64URL(Header).BASE64URL(Payload).signature is returned as a concatenated string.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

func GenToken(username string) (string, error) {

    // 创建声明

    a := &Claims{

        Username: username,

        StandardClaims: jwt.StandardClaims{

            ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间

            IssuedAt:  time.Now().Unix(),                 // 签发时间

            Issuer:    "gin-jwt-demo",                    // 签发者

            Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效

            NotBefore: 0,                                 // 生效起始时间

            Subject:   "",                                // 主题

        },

    }

    // 用指定的哈希方法创建签名对象

    tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)

    // 用上面的声明和签名对象签名字符串token

    // 1. 先对Header和PayLoad进行Base64URL转换

    // 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起

    // 3. 用secret对拼接在一起之后的字符串进行HASH加密

    // 4. 连在一起返回

    return tt.SignedString(Secret)

}

(2) ParseToken method

The ParseToken method parses a Token and verifies whether the Token is valid.

jwt.ParseWithClaims method, used to parse Token, its third parameter:

Provide a callback function to provide the secret key to be selected. The token parameter in the callback function has been parsed but not verified. You can do some logic based on the value in the token, such as judging the kid to select different secrets.

KID (optional): represents the secret key serial number. Developers can use it to identify a certain secret key of the authentication token

1

2

3

4

5

6

7

8

9

10

11

12

13

14

func ParseToken(tokenStr string) (*Claims, error) {

    // 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断

    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {

        return Secret, nil

    })

    if err != nil {

        return nil, err

    }

    // 校验token

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {

        return claims, nil

    }

    return nil, errors.New("invalid token")

}

Write middleware

Remove the Authorization from the Header and use it to parse jwt.ParseToken,

Verify whether the token has been altered and whether it has expired

Get valid information from the token and set it to the context

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

func JWTAuthMiddleware() func(ctx *gin.Context) {

    return func(ctx *gin.Context) {

        // 根据实际情况取TOKEN, 这里从request header取

        tokenStr := ctx.Request.Header.Get("Authorization")

        if tokenStr == "" {

            ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{

                "code": code.ERR_AUTH_NULL,

                "msg":  code.GetMsg(code.ERR_AUTH_NULL),

            })

            return

        }

        claims, err := jwt.ParseToken(tokenStr)

        if err != nil {

            ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{

                "code": code.ERR_AUTH_INVALID,

                "msg":  code.GetMsg(code.ERR_AUTH_INVALID),

            })

            return

        } else if time.Now().Unix() > claims.ExpiresAt {

            ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{

                "code": code.ERR_AUTH_EXPIRED,

                "msg":  code.GetMsg(code.ERR_AUTH_EXPIRED),

            })

            return

        }

        // 此处已经通过了, 可以把Claims中的有效信息拿出来放入上下文使用

        ctx.Set("username", claims.Username)

        ctx.Next()

    }

}

Use middleware

/login不用中间件

中间件指定在authorizedrouter, 因此authorized下的所有路由都会使用此中间件

1

2

3

4

5

6

7

8

9

10

func main() {

    r := gin.Default()

    r.POST("/login", router.Login)

    authorized := r.Group("/auth")

    authorized.Use(jwt.JWTAuthMiddleware())

    {

        authorized.GET("/getUserInfo", router.GetUserInfo)

    }

    r.Run(":8082")

}

测试

login请求获取token

POST http://localhost:8082/login

 

把token放入getUserInfo请求

GET  http://localhost:8082/auth/getUserInfo

 

其他

完整的JWT登录还应该包括

  • 使TOKEN失效(过期或者黑名单等功能)
  • refresh token

Guess you like

Origin blog.csdn.net/u013302168/article/details/132178429