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
都会同时保存在mysql
和redis
中,这里用线程安全的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
}
|
2.3 cookie
cookie
是浏览器实现的一种数据存储功能,cookie
通常由服务端创建,浏览器将cookie
以kv键值对
的形式保存到文件中,浏览器对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
的一种身份认证机制,是目前较为流行的跨域认证方案。它的数据结构如下所示一共包含三部分:Header
,Payload
,Signature
,其中payload
部分是我们实际需要传输的数据,官方也为我们提供一些官方字段可供选择,Header
,Payload
都是json
数据。
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官网