这是我参与「第三届青训营 -后端场」笔记创作活动的的第6篇笔记
前言
学习了一下原本的demo里面是怎么定义token的,就自己模仿着demo那个样子以用户名加密码拼接的方式作为token,结果群里有大佬说有个jwt的东西来生成token,那就找几篇博客学习一下
JWT
什么是JWT
JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token 实现方式,目前多用于前后端分离项目和 OAuth2.0 业务场景下。
为什么需要JWT
在之前的一些web项目中,我们通常使用的是Cookie-Session模式实现用户认证。相关流程大致如下:
- 用户在浏览器端填写用户名和密码,并发送给服务端
- 服务端对用户名和密码校验通过后会生成一份保存当前用户相关信息的session数据和一个与之对应的标识(通常称为session_id)
- 服务端返回响应时将上一步的session_id写入用户浏览器的Cookie
- 后续用户来自该浏览器的每次请求都会自动携带包含session_id的Cookie
- 服务端通过请求中的session_id就能找到之前保存的该用户那份session数据,从而获取该用户的相关信息。
这种方案依赖于客户端(浏览器)保存 Cookie,并且需要在服务端存储用户的session数据。
在移动互联网时代,我们的用户可能使用浏览器也可能使用APP来访问我们的服务,我们的web应用可能是前后端分开部署在不同的端口,有时候我们还需要支持第三方登录,这下Cookie-Session的模式就有些力不从心了。
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会生成一个JSON对象,经过签名后得到一个Token(令牌)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。 注意:在JWT中,不应该在载荷⾥⾯加⼊任何敏感的数据,⽐如⽤户的密码。
安装
go get github.com/golang-jwt/jwt/v4
使用
普通JWT
package main
import (
"github.com/golang-jwt/jwt/v4"
"time"
)
var mySigningKey = []byte("dousheng")
// 使用默认声明创建jwt
func GenRegisteredClaims() (string, error) {
// 创建Claims
claims := &jwt.RegisteredClaims{
Issuer: "lzd",
Subject: "",
Audience: nil,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 过期时间
NotBefore: nil,
IssuedAt: nil,
ID: "",
}
// 生成token对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(mySigningKey)
}
// 解析jwt
func ValidateRegisteredClaims(tokenString string) bool {
// 解析token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
return false
}
return token.Valid
}
func main() {
}
自定义claims
我们需要定制自己的需求来决定JWT中保存哪些数据,比如我们规定在JWT中要存储username信息,那么我们就定义一个MyClaims结构体如下:
// CustomClaims 自定义声明类型 并内嵌jwt.RegisteredClaims
// jwt包自带的jwt.RegisteredClaims只包含了官方字段
// 假设我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type CustomClaims struct {
// 可根据需要自行添加字段
Username string `json:"username"`
jwt.RegisteredClaims // 内嵌标准的声明
}
生成jwt
// GenToken 生成token
func GenToken(username string, uid uint) (string, error) {
// 创建一个自己的声明
claims := DouShengClaims{
Username: username,
UserID: uid,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireDuration)),
Issuer: "lzd",
},
}
// 使用指定签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(mySigningKey)
}
解析jwt
// ParseToken 解析Token
func ParseToken(tokenString string) (*DouShengClaims, error) {
// 解析token
// 如果自定义claim结构体需要使用
token, err := jwt.ParseWithClaims(tokenString, &DouShengClaims{}, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
return nil, err
}
// 对token对象中的claim进行类型断言
if claims, ok := token.Claims.(*DouShengClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
通过gin中间件在中间件中解析和放置数据
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//fmt.Printf("%+v\n", c.Request)
token := c.DefaultQuery("token", "")
//fmt.Println("in jwt middle, token : ", token)
if token == "" {
// 当无登陆状态时也会请求这两个方法
if c.Request.URL.Path[len(c.Request.URL.Path)-5:len(c.Request.URL.Path)-1] == "list" || c.Request.URL.Path[len(c.Request.URL.Path)-5:len(c.Request.URL.Path)-1] == "feed" {
return
}
token = c.PostForm("token")
if token == "" {
zap.L().Error("token is empty")
c.JSON(http.StatusUnauthorized, gin.H{
"status_code": 1,
"status_msg": "token is empty",
})
c.Abort()
return
}
}
// dc:DouShengClaims
dc, err := ParseToken(token)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status_code": 1,
"status_msg": err.Error(),
})
c.Abort()
return
}
c.Set("username", dc.Username)
c.Set("user_id", dc.UserID)
fmt.Println("jwt username : ", dc.Username)
fmt.Println("jwt user id : ", dc.UserID)
//c.Next()
}
}
模版
package middleware
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
"net/http"
"time"
)
var mySigningKey = []byte("dousheng")
const TokenExpireDuration = time.Hour * 24
// DouShengClaims 自定义Claims
type DouShengClaims struct {
// 自行添加的字段
Username string `json:"username"`
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// GenToken 生成token
func GenToken(username string, uid uint) (string, error) {
// 创建一个自己的声明
claims := DouShengClaims{
Username: username,
UserID: uid,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireDuration)),
Issuer: "lzd",
},
}
// 使用指定签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(mySigningKey)
}
// ParseToken 解析Token
func ParseToken(tokenString string) (*DouShengClaims, error) {
// 解析token
// 如果自定义claim结构体需要使用
token, err := jwt.ParseWithClaims(tokenString, &DouShengClaims{}, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
return nil, err
}
// 对token对象中的claim进行类型断言
if claims, ok := token.Claims.(*DouShengClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//fmt.Printf("%+v\n", c.Request)
token := c.DefaultQuery("token", "")
//fmt.Println("in jwt middle, token : ", token)
if token == "" {
// 当无登陆状态时也会请求这两个方法
if c.Request.URL.Path[len(c.Request.URL.Path)-5:len(c.Request.URL.Path)-1] == "list" || c.Request.URL.Path[len(c.Request.URL.Path)-5:len(c.Request.URL.Path)-1] == "feed" {
return
}
token = c.PostForm("token")
if token == "" {
zap.L().Error("token is empty")
c.JSON(http.StatusUnauthorized, gin.H{
"status_code": 1,
"status_msg": "token is empty",
})
c.Abort()
return
}
}
// dc:DouShengClaims
dc, err := ParseToken(token)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status_code": 1,
"status_msg": err.Error(),
})
c.Abort()
return
}
c.Set("username", dc.Username)
c.Set("user_id", dc.UserID)
fmt.Println("jwt username : ", dc.Username)
fmt.Println("jwt user id : ", dc.UserID)
//c.Next()
}
}