前言
作者大二,在网上看各种名词,什么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 }