这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
使用JWT
JWT是项目中比较常用的一种用来解决权限认证和校验的一种方法。
知识点
特点
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。 Token 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。 并且, 使用 Token 认证可以有效避免 CSRF 攻击,因为 Token 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。
组成
JWT 由哪些部分组成?
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:
-
头部 Header : 生成签名的算法以及
Token的类型。 -
负载 Payload : 用来存放实际需要传递的数据
-
签名 Signature(签名) : 服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
JWT 通常是这样的:xxxxx.yyyyy.zzzzz。
优缺点
优点 不依赖Cookie,使得其可以防止CSRF攻击 也能在禁用Cookie的浏览器环境中正常运行
(最大优势)服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度
缺点 JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。
配置
go get github.com/dgrijalva/jwt-go
项目结构
├─.idea
├─config
├─controller
├─dao
├─database
├─global
│ ├─consts
│ ├─my_errors
│ └─variable
├─main
├─model
├─router
├─service
│ └─user
│ ├─curd
│ └─token
├─test
└─utils
├─my_jwt
└─response
Bug:sql: Scan error on column index 5, name "create_time": unsupported Scan, storing driver.Value type []uint8 into type *time.Time
解决方法 使用 gorm 这样的第三方库时,它提供了自动转换时间字段的功能。 首先在初始化数据库连接时设置相应的自动转换选项,如下:
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
这将告诉 gorm 使用 MySQL 驱动程序并解析时间字段。 然后在结构体字段上使用 gorm:"column:create_time" 这样的标签来指定数据库字段的名称。
type User struct {
CreateTime time.Time `gorm:"column:create_time" json:"create_time"`
}
这样,gorm 就会自动地将数据库中的时间戳字段转换为 Go 中的 time.Time 类型。
代码
(关键代码) utils/my_jwt/my_jwt.go
package my_jwt
import "github.com/dgrijalva/jwt-go"
// 自定义jwt的声明字段信息+标准字段,参考地址:https://blog.csdn.net/codeSquare/article/details/99288718
type CustomClaims struct {
UserID uint64 `json:"user_id"` // user_id
Username string `json:"username"` // username
jwt.StandardClaims
}
utils/my_jwt/custom_claims.go
package my_jwt
import (
"errors"
"github.com/dgrijalva/jwt-go" "go_douyin/global/my_errors" "time")
// 使用工厂创建一个 JWT 结构体
func CreateMyJWT(signKey string) *JwtSign {
if len(signKey) <= 0 {
signKey = "douyin217@qqTEAM"
}
return &JwtSign{
[]byte(signKey),
}
}
// 定义一个 JWT验签 结构体
type JwtSign struct {
SigningKey []byte
}
// CreateToken 生成一个token
func (j *JwtSign) CreateToken(claims CustomClaims) (string, error) {
// 生成jwt格式的header、claims 部分
tokenPartA := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 继续添加秘钥值,生成最后一部分
return tokenPartA.SignedString(j.SigningKey)
}
// 解析Token
func (j *JwtSign) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if token == nil {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.New(my_errors.ErrorsTokenMalFormed)
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, errors.New(my_errors.ErrorsTokenNotActiveYet)
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// 如果 TokenExpired ,只是过期(格式都正确),我们认为他是有效的,接下可以允许刷新操作
token.Valid = true
goto labelHere
} else {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
}
}
labelHere:
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
} else {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
}
// 更新token
func (j *JwtSign) RefreshToken(tokenString string, extraAddSeconds int64) (string, error) {
if CustomClaims, err := j.ParseToken(tokenString); err == nil {
CustomClaims.ExpiresAt = time.Now().Unix() + extraAddSeconds
return j.CreateToken(*CustomClaims)
} else {
return "", err
}
}
service/user/token/token.go
package token
import (
"errors"
"github.com/dgrijalva/jwt-go" "go_douyin/global/my_errors" "go_douyin/utils/my_jwt" "time")
// CreateUserFactory 创建 userToken 工厂
func CreateUserFactory() *userToken {
return &userToken{
//userJwt: my_jwt.CreateMyJWT(variable.ConfigYml.GetString("Token.JwtTokenSignKey")),
userJwt: my_jwt.CreateMyJWT("12314@"),
}
}
type userToken struct {
userJwt *my_jwt.JwtSign
}
// GenerateToken 生成token
func (u *userToken) GenerateToken(userid uint64, username string, expireAt int64) (tokens string, err error) {
// 根据实际业务自定义token需要包含的参数,生成token,注意:用户密码请勿包含在token
customClaims := my_jwt.CustomClaims{
UserID: userid,
Username: username,
// 特别注意,针对前文的匿名结构体,初始化的时候必须指定键名,并且不带 jwt. 否则报错:Mixture of field: value and value initializers
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix() - 10, // 生效开始时间
ExpiresAt: time.Now().Unix() + expireAt, // 失效截止时间
},
}
return u.userJwt.CreateToken(customClaims)
}
// ParseToken 将 token 解析为绑定时传递的参数
func (u *userToken) ParseToken(tokenStr string) (CustomClaims my_jwt.CustomClaims, err error) {
if customClaims, err := u.userJwt.ParseToken(tokenStr); err == nil {
return *customClaims, nil
} else {
return my_jwt.CustomClaims{}, errors.New(my_errors.ErrorsParseTokenFail)
}
}