本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
作者大二,在网上看各种名词,什么cookie,session,JWT,看了几篇文章后,概念是理解了,但是真正想写一个小demo实践的时候,==总是感觉无从下手==,所以写本文的目的,意在帮助在与我同阶段的同学,==从0到1的去梳理出流程,以提升自己的抽象和分析能力==。
前置要求:文demo使用了gin和gorm,如果不知道gin和gorm两个库,如果没有使用过的需要提前去了解一下基本用法即可 //TODO 挖坑 gin 和 gorm 相关内容 后续补充 GORM汇总
demo链接:gopherWxf-wake
JWT介绍
背景
在如今前后端分离开发的⼤环境中,我们需要解决⼀些登陆,后期身份认证以及鉴权相关的事情,通常的⽅案就是采⽤请求头携带token的⽅式进⾏实现。本篇⽂章主要分享下在Golang语⾔下使⽤jwt-go来实现后端的token认证逻辑。 JSON Web Token(JWT) 是⼀个常⽤语HTTP的客户端和服务端间进⾏身份认证和鉴权的标准规范,使⽤JWT可以允许我们在⽤户和服务器之间传递安全可靠的信息。在开始学习JWT之前,我们可以先了解下早期的⼏种⽅案。
token
token
token的意思是“令牌”,是⽤户身份的验证⽅式,最简单的token组成: uid(⽤户唯⼀标识) + time(当前时间戳) + sign(签名,由token的前⼏位+哈希算法压缩成⼀定⻓度的⼗六进制字符串) ,同时还可以将不变的参数也放进token,这⾥我主要想讲的就是 Json Web Token ,也就是本文的主题:JWT
Json-Web-Token(JWT)介绍
⼀般⽽⾔,⽤户注册登陆后会⽣成⼀个jwt的token返回给浏览器,浏览器向服务端请求数据时携带token ,服务器端使⽤ header 中定义的⽅式进⾏解码,进⽽对token进⾏解析和验证。
JWT-Token组成部分:
- ==header==: ⽤来指定使⽤的算法(HMAC SHA256 RSA)和token类型(如JWT)
- ==payload==: 包含声明(要求),声明通常是⽤户信息或其他数据的声明,⽐如⽤户id,名称,邮箱等
- ==signature==: signature签名部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
签名是(signature) ⽤于验证消息在传递过程中有没有被更改 ,并且,对于使⽤私钥签名的token,它还可以验证JWT的发送⽅是否为它所称的发送⽅。
注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
- header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
- signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secret对header、payload进行加密,==比对加密后的数据和客户端发送过来的是否一致==。注意secret只能保存在服务端。
⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。 注意:在JWT中,不应该在载荷⾥⾯加⼊任何敏感的数据,⽐如⽤户的密码。
JSON Web TokenS - jwt.io在jwt.io⽹站中,提供了⼀些JWT token的编码,验证以及⽣成jwt的⼯具。
可以看到header 和 payload 部分可以直接解码出来
此时签名为kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8'
kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8=HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
如果篡改了header或者payload的内容,那么计算hs256的值一定不一样
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
介绍完jwt的原理,下面我们就该动手实践了!
Demo代码分析
流程分析
流程分析:
- 什么时候创建token并返回给前端呢?用户登陆的时候,因为接下来的网页用户不需要再次登陆才能访问了
- 前端用户携带token访问后端的时候,后端需要干什么呢?后端需要校验token是否合法,如果合法则继续执行业务逻辑,如果不合法直接返回错误,中断本次业务逻辑
好,初步的流程是
- 登陆的时候后端创建token返回给前端
- 除了登陆和注册的接口,其他接口都携带该token访问后端
框架分析
我们首先需要对demo的项目注册有个大概的了解
demo演示
注册:
登陆:
访问测试接口:
更新token时间:
后端日志打印:
走入源码
入口main:
首先我们的程序需要连接数据库(因为用户注册和登陆需要数据库),定义4个接口
POST /apis/v1/register --> jwtDemo/controller.RegisterUser //注册
POST /apis/v1/login --> jwtDemo/controller.Login //登陆
下面两个接口都有一个token校验的中间件sv1.Use(middleware.JWTAuth)
GET /apis/v1/auth/sayHello --> jwtDemo/controller.SayHello //测试
GET /apis/v1/auth/refresh --> jwtDemo/controller.Refresh //更新token
func main() {
//从配置文件中读取数据库的配置信息并连接数据库
if dbErr := opdb.InitMySqlConn(); dbErr != nil {
log.Panicln(dbErr)
}
defer opdb.DB.Close()
//初始化表结构
opdb.InitModel()
router := gin.Default()
v1 := router.Group("apis/v1")
{ //注册
v1.POST("/register", controller.RegisterUser)
//登陆,返回token
v1.POST("/login", controller.Login)
}
sv1 := v1.Group("/auth")
//检验token
sv1.Use(middleware.JWTAuth)
{ //测试
sv1.GET("/sayHello", controller.SayHello)
//更新token
sv1.GET("/refresh", controller.Refresh)
}
router.Run(":8080")
}
-
controller.RegisterUser:前端返回的json,包含用户名,密码,手机号,地址等然后后端存入数据库
-
controller.Login:通过前端返回的json,查询数据库中是否有该用户,有则创建一个json并返回
-
middleware.JWTAuth:校验改token是否合法,如果合法则继续执行,不合法直接结束流程
-
controller.SayHello:通过了JWTAuth的校验,改前端返回一个hello wxf
-
controller.Refresh:通过了JWTAuth的校验,给token续费,返回一个新的token
好,现在各个回调函数的流程都知道了,我们详细来看看创建token和校验token的代码
创建token:
- 创建一个包含secret的结构体
- 构造用户claims信息(负荷jwt中的第二部分)
- 通过secret密钥返回token
//验证账号密码是否正确,即是否在数据库中存在,注册过
pass, dbErr := opdb.LoginPass(loginReq)
if !pass {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "账号或密码错误" + dbErr.Error(),
"data": nil,
})
return
}
//创建一个token
token := generateToken(c, loginReq)
//创建一个token
func generateToken(c *gin.Context, loginReq dfst.LoginReq) (token string) {
// 构造SignKey: 签名和解签名需要使用一个值
jwt := middleware.NewJWT()
// 构造用户claims信息(负荷)
claims := middleware.CustomClaims{
Name: loginReq.Name,
StandardClaims: jwt2.StandardClaims{
NotBefore: time.Now().Unix() - 1000, // 签名生效时间
ExpiresAt: time.Now().Unix() + 3600, // 签名过期时间
Issuer: "wxf.top", // 签名颁发者
},
}
// 根据claims生成token对象
token, err := jwt.CreateToken(claims)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
}
log.Println("create token", token)
return
}
//创建token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
//获取完整的签名令牌
return token.SignedString(j.SigningKey)
}
校验token:
- 从header的头部的token字段获取token
- 解析token,并将PAYLOAD负载提取出来
- 将负载添加到context上下文中供调用链中的函数使用
func JWTAuth(c *gin.Context) {
//从header的头部的token字段获取token
token := c.Request.Header.Get("token")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "请求未携带token,无权限访问",
"data": nil,
})
c.Abort()
return
}
log.Println("recv tokens:", token)
j := NewJWT()
//解析token,并将PAYLOAD负载提取出来
claims, err := j.ParserToken(token)
if err != nil {
// token过期
if err == TokenExpired {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "token授权已过期,请重新申请授权",
"data": nil,
})
//中断调用链
c.Abort()
return
}
// 其他错误
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
c.Abort()
return
}
//将负载添加到context上下文中供调用链中的函数使用
c.Set("claims", claims)
}
//解析token,并将PAYLOAD负载提取出来
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
// jwt.ValidationError 是一个无效token的错误结构
if ve, ok := err.(*jwt.ValidationError); ok {
// ValidationErrorMalformed是一个uint常量,表示token不可用
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
// ValidationErrorExpired表示Token过期
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, TokenExpired
// ValidationErrorNotValidYet表示无效token
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
}