likes
comments
collection
share

Go 实践|基于 Token 的登录流程

作者站长头像
站长
· 阅读数 14

背景

基于 Token 认证机制

Token 认证机制其实就是服务端将认证信息返回给客户端,客户端访问其他页面时都需要携带认证信息给服务端,由服务端验证这个认证信息是否有效可使用。大致流程:

  • 使用账号和密码请求登录,服务端接收到账号和密码进行验证
  • 验证成功,就会签发一个Token,返回给客户端
  • 前端接收Token并存储,后续客户端再请求其他页面都携带 Token
  • 服务端接收到客户端的请求时,先验证请求中携带的 token,如果验证成功,就向客户端返回请求的数据

Token 生成和验证

现在网上有很多关于 JWT 的使用说明可以直接搜索使用。其中, Token = Header + '.' + Payload + '.' + Signature。

  • Header 是用来说明签名的加密算法
{
    "typ":"jwt"
    "alg":"HS256"
}
  • Payload 是记录我们可能需要的信息,除了现有已提供的标准的Claims ,还可以自定义一些可能需要的字段,比如 userId,userName 等。claims 是可以被解密的,所以不要存放重要而敏感的的信息
// --- 标准
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。如用户名称、签发时间、过期时间。

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
}

创建签名

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))
}

验证签名

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。