用户认证模式Cookie-Session、JWT-Token(gin框架实现)

在计算机网络中,我们知道HTTP是一个无状态的协议,一次请求结束后,下次再发送服务器就不知道这个请求是谁发来的了(同一个IP不代表同一个用户),在Web应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用模式,并且各有千秋。

Cookie-Session认证模式

简介

在Web应用发展的初期,大部分采用基于Cookie-Session的会话管理方式

  • 客户端使用用户名、密码进行认证
  • 服务端验证用户名、密码正确后生成并存储Session,将SessionID通过Cookie返回给客户端
  • 客户端访问需要认证的接口时在Cookie中携带SessionID
  • 服务端通过SessionID查找Session并进行鉴权,返回给客户端需要的数据
    在这里插入图片描述

代码示例

package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	"log"
	"net/http"
	"time"
)

var Rdb *redis.Client //redis全局变量

type UserLogin struct {
    
     //登录入参
	UserName string `json:"user_name"`
	Password string `json:"password"`
}

func redisStart() {
    
    
	rdb := redis.NewClient(&redis.Options{
    
    
		Addr:     "127.0.0.1:6379",
		Password: "123456",
		DB:       0,
		PoolSize: 100,
	})
	_, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()
	ctx := context.Background()
	pong, err := rdb.Ping(ctx).Result()
	fmt.Println(pong)
	if err != nil {
    
    
		log.Println(err)
	}
	Rdb = rdb
}

func main() {
    
    
	redisStart()
	router := gin.Default()
	// 设置登录请求的路由处理函数
	router.POST("/login", loginHandler)

	// 设置受保护页面的路由处理函数
	router.GET("/protected", protectedHandler)

	router.Run(":8080")
}

func loginHandler(c *gin.Context) {
    
    

	var user UserLogin
	if err := c.ShouldBindJSON(&user); err != nil {
    
    
		log.Println(err)
		return
	}
	// 模拟检查用户名和密码是否匹配
	// 这里应该是与数据库中的用户名和密码进行比对
	if user.UserName == "用户的用户名" && user.Password == "用户的密码" {
    
    
		//生成一个Session ID
		sessionID := "你自己设置的sessionID"
		// 将Session ID 作为键 存储到redis中,并设置过期时间(此处为30分钟)
		Rdb.Set(context.Background(), sessionID, "你想存储的用户信息", time.Minute*30)
		//创建Cookie ,将Session ID 设置为Cookie的值
		/*
			name:"session"
			value:sessionID
			失效时间:3600
			path:指定cookie在哪个路径(路由)下生效,默认是`\`
			domain:指定cookie所属域名,默认是当前域名
			secure:该cookie是否被使用安全协议传输。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false
			httpOnly:如果给某个cookie设置了httpOnly属性,则无法通过js脚本读取到该cookie的信息。
		*/
		c.SetCookie("session", sessionID, 3600, "/", "localhost", false, true)
		c.Redirect(http.StatusFound, "/protected")
	} else {
    
    
		// 登录失败
		c.String(http.StatusUnauthorized, "Invalid username or password")
	}
}
func protectedHandler(c *gin.Context) {
    
    
	//检查是否存在session cookie
	cookie, err := c.Cookie("session")
	if err != nil || cookie == "" {
    
    
		c.Redirect(http.StatusOK, "/login")
		return
	}
	//检查session是否存在且过期
	isOk := Rdb.Exists(context.Background(), cookie).Val()
	duration := Rdb.TTL(context.Background(), cookie).Val()
	if isOk == 0 || duration <= 0 {
    
    
		c.Redirect(http.StatusFound, "/login")
		return
	}
	// 受保护页面的逻辑
	c.String(http.StatusOK, "Welcome to the protected page!")
}

优缺点

优点:

  • session-cookie 认证机制在基本上所有的网页浏览器上都能够支持
  • 实现方式简单

缺点:

  • 服务端需要存储Session,并且由于Session需要经常快速查找,通常存储在内存或内存数据库中 ,如果在线用户的人数较多时,会占用大量的服务器资源。
  • 当需要扩展时,创建Session的服务器可能不是验证Session的服务器,所以还需要将所有Session单独存储并共享。
  • 由于客户端使用Cookie存储SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范CSRF攻击。

Token认证模式

简介

鉴于基于Session的会话管理方式存在上述的多个缺点,基于Token的无状态(服务端不存储信息)会话方式诞生了。
所谓的Token,其实就是服务端生成的一串加密字符串、以作客户端进行请求的一个“令牌”。当用户第一次使用账号密码成功进行登录后,服务器便生成一个Token及Token失效时间并将此返回给客户端,若成功登陆,以后客户端只需在有效时间内带上这个Token前来请求数据即可,无需再次带上用户名和密码。

逻辑如下:

  • 客户端使用用户名、密码进行认证

  • 服务端验证用户名、密码正确后生成Token返回给客户端

  • 客户端保存Token,访问需要认证的接口是在URL参数或HTTP Header中加入Token

  • 服务端通过解码Token进行鉴权,返回给客户端需要的数据
    在这里插入图片描述
    基于Token的会话管理方式有效解决了基于Session的会话管理方式带来的问题。

  • 服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到Token中,服务端只需要读取Token中包含的鉴权信息即可

  • 避免了共享Session导致的不易扩展问题

  • 不需要依赖Cookie,有效避免Cookie带来的CSRF攻击问题

  • 使用CORS可以快速解决跨域问题

JWT介绍

JSON Web Token (JWT) 是一种为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准 ( RFC 7519 ),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

JWT本身没有定于任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO)场景。

以下是 JSON Web 令牌使用的一些场景:

  • 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够轻松地跨不同域使用。
  • 信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以进行签名(例如,使用公钥/私钥对),所以您可以确定发送者就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

JWT结构

JWT令牌是由点.分隔的三个部分组成:

  • 标头(Header)
  • 负载(Payload)
  • 签名(Signature

JWT通常如下所示:

xxxxx.yyyyy.zzzzz

在这里插入图片描述

头部和负载以JSON的形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.拼接成一个JWT Token

标头(Header)

标头(Header)通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(例如HMAC、SHA256或RSA)。

例如:

{
    
    
  "alg": "HS256",
  "typ": "JWT"
}

对该JSON进行Base64Url编码形成JWT的第一部分。

负载(Payload)

令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。
声明分为三种类型:注册声明、公开声明和私人声明。

  • 注册声明:这些是一组预定义的声明,不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。

iss(issuer):签发人/发行者
exp(expiration time):过期时间
sub(subject):主题
aud(audience):受众
nbf(Not Before):生效时间
iat(Issued At):签发时间
jti(JWT ID):编号

声明名称只有三个字符长,因为JWT旨在紧凑。

  • 公共声明:这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在IANA JSON Web 令牌注册表中定义,或者定义为包含防冲突命名空间的 URI。
  • 私人声明:为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。

负载实例:

{
    
    
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。

注意,对于签名令牌,此信息虽然受到防止篡改的保护,但任何人都可以读取。除非加密,否则请勿将秘密信息放入 JWT 的有效负载或标头元素中。

签名(Signature)

要创建签名部分,您必须获取编码的标头、编码的有效负载、密钥、标头中指定的算法,然后对其进行签名。

例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:

你需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256)按照下面的公式产生签名。

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

代码示例

// JwtPayLoad jwt中payload数据
type JwtPayLoad struct {
    
    
	Username string `json:"username"`  //用户名
	NickName string `json:"nick_name"` //昵称
	Role     int    `json:"role"`      // 权限 1 管理员 2 普通用户 3 游客
	UserID   uint   `json:"user_id"`   //用户id
}

type CustomClaims struct {
    
    
	JwtPayLoad
	jwt.StandardClaims
}

// GenToken 创建token
func GenToken(user JwtPayLoad) (string, error) {
    
    
	var MySecret = []byte(global.Config.Jwy.Secret)
	claim := CustomClaims{
    
    
		user,
		jwt.StandardClaims{
    
    
			ExpiresAt: jwt.At(time.Now().Add(time.Hour * time.Duration(global.Config.Jwy.Expires))), //默认2小时过期
			Issuer:    global.Config.Jwy.Issuer,                                                     // 签发人
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
	return token.SignedString(MySecret)
}

// ParseToken 解析token
func ParseToken(tokenStr string) (*CustomClaims, error) {
    
    
	var MySecret = []byte(global.Config.Jwy.Secret)
	token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{
    
    }, func(token *jwt.Token) (interface{
    
    }, error) {
    
    
		return MySecret, nil
	})
	if err != nil {
    
    
		global.Logger.Error(fmt.Sprintf("token parse errr: %s", err.Error()))
		return nil, err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
    
    
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

JWT优缺点

JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。

而JWT的最大优势是服务器不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储在Token中,JWT Token一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单独使用JWT就无法做到了。

Access Token和Refresh Token认证模式

前面提到的Token,都是Access Token,也就是访问资源接口时所需要的Token,还有另外一种Token,Refresh Token,通常情况下,Refresh Token的有效期会比较长,而Access Token的有效期比较短,当Access Token由于过期而失效时,使用Refresh Token就可以获取到新的Access Token,如果Refresh Token也失效了,用户就只能重新登录了。

  • 客户端使用用户名密码进行认证
  • 服务端生成有效时间较短的Access Token(例如10分钟),和有效时间较长的Refresh Token(例如7天)
  • 客户端访问需要认证的接口时,携带Access Token
  • 如果Access Token没有过期,服务端鉴权后返回给客户端需要的数据
  • 如果携带Access Token访问需要认证的接口时鉴权失败(例如返回401错误),则客户端使用Refresh Token没有过期,服务端向客户端发新的Access Token
  • 如果Refresh Token没有过期,服务端向客户端发新的Access Token
  • 客户端使用新的Access Token访问需要认证的接口
    在这里插入图片描述

代码示例


//AccessClaims 
func (j *JWT) CreateAccessClaims(baseClaims request.BaseClaims) request.CustomClaims {
    
    
	accessExpires, _ := time.ParseDuration(setting.Conf.JWT.AccessExpiresTime)
	claims := request.CustomClaims{
    
    
		TypeClaims: "accessClaims",
		BaseClaims: baseClaims,
		RegisteredClaims: jwt.RegisteredClaims{
    
    
			IssuedAt:  jwt.NewNumericDate(time.Now()),                    //签发时间
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessExpires)), // 过期时间 7天  配置文件
			Issuer:    setting.Conf.JWT.Issuer,                           // 签名的发行者
		},
	}
	return claims
}

//refreshClaims
func (j *JWT) CreateRefreshClaims(baseClaims request.BaseClaims) request.CustomClaims {
    
    
	RefreshExpires, _ := time.ParseDuration(setting.Conf.JWT.RefreshExpiresTime)
	claims := request.CustomClaims{
    
    
		TypeClaims: "refreshClaims",
		RegisteredClaims: jwt.RegisteredClaims{
    
    
			IssuedAt:  jwt.NewNumericDate(time.Now()),                     //签发时间
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshExpires)), // 过期时间   配置文件
			Issuer:    setting.Conf.JWT.Issuer,                            // 签名的发行者
		},
	}
	return claims
}

// CreateToken 创建一个token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
    
    
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey) //SigningKey 秘钥 自己定义
}


// ParseToken 解析 token
func ParseToken(tokenStr string) (*CustomClaims, error) {
    
    
	var MySecret = []byte(global.Config.Jwy.Secret)
	token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{
    
    }, func(token *jwt.Token) (interface{
    
    }, error) {
    
    
		return MySecret, nil
	})
	if err != nil {
    
    
		global.Logger.Error(fmt.Sprintf("token parse errr: %s", err.Error()))
		return nil, err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
    
    
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

猜你喜欢

转载自blog.csdn.net/m0_53328239/article/details/131724576