目录

session和Jwt实现用户认证

1. 用户认证

由于http是无状态的,无法维护前后端的会话信息,即无法辩别上次请求发送者和这次请求发送者是不是同一个人,例如一个网站,用户发起了登录请求成功登录了网站,用户只有登录网站之后才会有后续的一些请求,对于后面的请求,服务端并不能分辨出请求是来自哪个人,就不能基于用户去做一些事(用户购物车增删,用户余额扣款),这时就需要引入认证机制去跟踪会话,让服务端知道是谁在访问我

2. Session

2.1 基于Session的用户认证流程
  • 1.前端发送用户名和密码给服务端以登录系统
  • 2.服务端验证通过后,创建一个session,用于保存用户信息(通常是用户名)和过期时间或登录时间(session可以保存到内存数据库中)
  • 3.服务端返回sessionId,前端将其存入cookie
  • 4.以后每次请求通过cookie,携带sessionId
  • 5.服务端收到sessionId后,查询出用户信息
2.2 session 缺点
  • 服务端需要维护session信息,消耗大量内存
  • 扩展性差:分布式部署时,需要考虑使用redis和其他数据库来保存session信息
2.3 golang代码实现

以下代码用go语言实现了服务端生成session,检验session是否过期,并根据sessionId查询用户名的功能,考虑到session会被频繁的查询,一般session都会同时保存在mysqlredis中,这里用线程安全的map代替redis

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package session

import (
	"github.com/gofrs/uuid"
	"gofun/def"
	"sync"
	"time"
)

type SimpleSession struct {
	UserName string
	TTL      int64 // session过期时间
}

// 使用map保存session
var sessionMap sync.Map

func init() {
	sessionMap = sync.Map{}
}

// 获取毫秒级时间戳
func NowInMilli() int64 {
	return time.Now().UnixNano() / 1000000
}

func GenerateNewSession(userName string) string {
	sid := uuid.Must(uuid.NewV4()).String()

	ttl := NowInMilli() + 1000*60*30 //30min

	ss := &def.SimpleSession{
		UserName: userName,
		TTL:      ttl,
	}

	// todo :插入数据库
	sessionMap.LoadOrStore(sid, ss)
	return sid
}

// 程序重启后调用
func LoadSessionFromDB() {
	// todo:从数据库中取出session导入
}

// delete
func DeleteExpireSession(sid string) {
	// todo :数据库删除
	sessionMap.Delete(sid)

}

// SessionIsExpire:判断session是否过期
func SessionIsExpire(sid string) (string, bool) {
	v, ok := sessionMap.Load(sid)
	if ok {
		if v.(*def.SimpleSession).TTL < NowInMilli() {
			DeleteExpireSession(sid)
			return "", true

		}
		return v.(*def.SimpleSession).UserName, false
	}
	return "", true
}

cookie是浏览器实现的一种数据存储功能,cookie通常由服务端创建,浏览器将cookiekv键值对的形式保存到文件中,浏览器对cookie进行了一些限制,不会占用太多的磁盘空间,所以每个域名中cookies的数量是有限的。

3.Token

session需要服务端来维护session会话,这样扩展性就不好,如果我们想部署分布式应用由于session信息默认只存在在当前的服务器上,而请求又被负载到不同的服务器上;其次如果用户数据需要和第三方共享,或者允许第三方调用系统的API接口,这时基于token做认证和授权就相对方便了。

3.1 token流程
  • 1.前端发送用户名和密码给服务端以登录系统
  • 2.服务端验证通过后,签发一个token,将用户信息编码进token中,服务端不会保存这个token
  • 3.服务端返回token,前端将其存入cookie
  • 4.以后每次请求通过cookie,携带token
  • 5.服务端收到token后,解析出用户信息
3.1 Jwt

Jwt(Json Web token)就是基于token的一种身份认证机制,是目前较为流行的跨域认证方案。它的数据结构如下所示一共包含三部分:HeaderPayloadSignature,其中payload部分是我们实际需要传输的数据,官方也为我们提供一些官方字段可供选择,Header,Payload都是json数据。

https://z3.ax1x.com/2021/04/26/gpS2fH.jpg

3.2 Jwt的优缺点
3.2.1 优点
  • 服务端不用维护session状态,tokent由客户端保存,减少了服务端的查询负载或没有查询负载
3.2.2 缺点
  • Jwt保存在客户端,且仅经过base64编码,不能保存私密数据(签名只是为了验证数据不被篡改)
  • 无法废弃,当需要修改Jwt内容时,需要重新签发一个新的token
  • 服务端需要增加Jwt过期和续签机制
3.3 golang代码实现
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// JWTAuth 中间件,检查token
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.Request.Header.Get("token")
		if token == "" {
			middleware.ResponseError(c, -1, errors.New("请求未携带token,无权限访问"))
			c.Abort()
			return
		}
		log.Print("get token: ", token)
		j := NewJWT()
		// parseToken 解析token包含的信息
		claims, err := j.ParseToken(token)
		fmt.Println("claims", claims)
		if err != nil {
			if err == TokenExpired {
				middleware.ResponseError(c, -1, errors.New("授权已过期"))
				c.Abort()
				return
			}
			middleware.ResponseError(c, -1, err)
			c.Abort()
			return
		}
		// 继续交由下一个路由处理,并将解析出的信息传递下去
		c.Set("claims", claims)
	}
}

*/

// JWT 签名结构
type JWT struct {
	// 签名密钥
	SigningKey []byte
}

// 定义好错误类型和签名密钥
var (
	TokenExpired     error  = errors.New("Token is expired")
	TokenNotValidYet error  = errors.New("Token not active yet")
	TokenMalformed   error  = errors.New("That's not even a token")
	TokenInvalid     error  = errors.New("Couldn't handle this token:")
	SignKey          string = "gofun"
)

// 载荷,可以加一些自己需要的信息
type CustomClaims struct {
	// 自定义私有字段
	ID       int    `json:"user_id"`
	UserName string `json:"user_name"`
	Password string `json:"telephone"`
	// 官方字段
	jwt.StandardClaims
}

// 新建一个jwt实例
func NewJWT() *JWT {
	return &JWT{
		[]byte(GetSignKey()),
	}
}

// 获取signKey
func GetSignKey() string {
	return SignKey
}

// 设置SignKey
func SetSignKey(key string) string {
	SignKey = key
	return SignKey
}

// CreateToken:生成一个token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey)
}

// 解析Tokne
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {

	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				// Token is expired
				return nil, TokenExpired
			} 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
}

// 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
	jwt.TimeFunc = func() time.Time {
		return time.Unix(0, 0)
	}
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		jwt.TimeFunc = time.Now
		claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
		return j.CreateToken(*claims)
	}
	return "", TokenInvalid
}

参考文档

[1] JSON Web Token 入门教程

[2] 傻傻分不清之 Cookie、Session、Token、JWT

[3] Jwt官网