背景
上一篇的文章沉淀了基于 session 的登录流程:Go 实践|基于 session 的登录流程 - 掘金,但是你会发现在分布式服务和单点登录等功能时使用 session 机制完成登录认证过程就需要解决共享和同步 session的问题。在现如今的发展的情况下,session 机制已经无法满足一些服务架构。
基于 Token 认证机制
Token 认证机制其实就是服务端将认证信息返回给客户端,客户端访问其他页面时都需要携带认证信息给服务端,由服务端验证这个认证信息是否有效可使用。大致流程:
- 使用账号和密码请求登录,服务端接收到账号和密码进行验证
- 验证成功,就会签发一个Token,返回给客户端
- 前端接收Token并存储,后续客户端再请求其他页面都携带 Token
- 服务端接收到客户端的请求时,先验证请求中携带的 token,如果验证成功,就向客户端返回请求的数据
Token 生成和验证
现在网上有很多关于 JWT 的使用说明可以直接搜索使用。其中, Token = Header + '.' + Payload + '.' + Signature。
- Header 是用来说明签名的加密算法
json
{
"typ":"jwt"
"alg":"HS256"
}
- Payload 是记录我们可能需要的信息,除了现有已提供的标准的Claims ,还可以自定义一些可能需要的字段,比如 userId,userName 等。claims 是可以被解密的,所以不要存放重要而敏感的的信息
erlang
// --- 标准
iss (issuer): JWT 的签发人
exp (expiration time): 过期时间
sub (subject): 主题,该jwt所面向的用户
aud (audience): 受众 接收该 jwt的一方
nbf (Not Before): 生效时间
iat (Issued At): 签发时间
jti (JWT ID): 编号
// 自定义 Claims
userId
userName
- Signature 是由Header 中声明的加密算法进行生成的签名
实践
创建 jwtMaker 文件,定义 Maker 接口来规范 jwtMaker 只需要进行创建 CreateToken 和验证 VerifyToken两个方法。可以使用现在比较常用的一个jwt 包来完成 Token 的 ,直接获取:go get github.com/dgrijalva/jwt-go
。
声明Claims
定义一个标识用户信息的结构体,这部分也是所需要的 Claims。如用户名称、签发时间、过期时间。
go
type CustomClaims struct {
ID uuid.UUID
UserName string
IssuedAt time.Time
ExpiresAt time.Time
}
func NewCustomClaims(userName string, duration time.Duration) (*CustomClaims, error) {
tokenId, err := uuid.NewRandom()
if err != nil {
return nil, err
}
customClaim := &CustomClaims{
ID: tokenId,
UserName: userName,
IssuedAt: time.Now(),
ExpiresAt: time.Now().Add(duration),
}
return customClaim, nil
}
func (c *CustomClaims) Valid() error {
if time.Now().After(c.ExpiresAt) {
return errors.New("token has expired")
}
return nil
}
创建签名
go
func (j *JwtMaker) CreateToken(userName string, duration time.Duration) (string, error) {
payload, err := NewCustomClaims(userName, duration)
if err != nil {
return "", err
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
return jwtToken.SignedString([]byte(j.SecretKey))
}
验证签名
go
func (j *JwtMaker) VerifyToken(token string) (*CustomClaims, error) {
var result = new(CustomClaims)
jwtToken, err := jwt.ParseWithClaims(token, result, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("无效 Token")
} else {
return []byte(j.SecretKey), nil
}
})
if err != nil {
return nil, err
}
// 校验 token
if jwtToken.Valid {
return result, nil
}
if err = result.Valid(); err != nil {
return nil, fmt.Errorf("token 已过期")
}
return nil, fmt.Errorf("invalid token")
}
验证签名时,通过解析 token ,验证 token 是否有效,再验证是否过期。验证Token 是用在用户登录后所有请求都需要携带 token ,然后服务端获取到 token 再进行验证过。
多设备登录登出
使用 Token 进行无状态认证后,就可以更方便实现多端登录。在 Claims 中可以加入签发 Token 的机器信息,这样就可以有效的控制多端的登录登出多种场景。
- 允许多端同时登录,任意端退出时其他端不退出:每个端生成都生成自己的Token,互相独立。
- 允许多端同时登录,任意端退出其他端也退出:增加用户设备表,记录用户的登录设备和对应的Token ,当退出时将该用户所有的Token 都失效。
- 多端选择性退出部分端:退出登录时增加一个需要退出端的对应设备类型或者设备ID,然后对应设备下的token 进行失效处理。
等等还有其他的使用场景,使用token 可以自由安全支持多端登录的机制。
最后
session 是用于记录服务器和客户端会话状态的机制。session 存储在服务端中,但当在集群或微服务架构中,多个服务端都在提供服务时,用户要获取自己的会话状态时,无论请求到那个服务端时都必须获取到自己相关的唯一 session 信息,所以此时需要多个服务端共享这个session。所以,如果将 session 存储再缓存中,就可以通过这种方式共享session。