上文已经介绍了jwt的基本原理和用法,44.jwt在go中的使用及原理(一),本文将介绍双token机制,以及sso单点登录
一、双Token机制
基于token安全性的处理 access token 和 refresh token
以下access token简称 atoken,refresh token 简称 rtoken。无感刷新方式。
在用户登录的时候颁发两个token,atoken 和 rtoken。atoken 的有效期很短,根据业务实际需求可以自定义。一般设置为10分钟足够。rtoken有效期较长,一般可以设置为一星期或者一个月,根据实际业务需求可以自行定义。(根据查询资料得知 rtoken需要进行client-sercet才能有效)。当atoken过期之后可以通过rtoken进行刷新,但是rtoken过期之后,只能重新登录来获取。
当atoken丢失之后没关系,因为它有效期很短。当rtoken丢失之后也没关系,因为他需要配合client-sercet才能使用。
在生成token时,我们一次生成两个token,atoken用于认证,会包含有用户相关信息,如UserID,Username等, 而rtoken不会保存用户信息,专门用于刷新atoken
// GenToken 颁发token access token 和 refresh token
func GenToken(UserID int64, Username string) (atoken, rtoken string, err error) {
rc := jwt.RegisteredClaims{
ExpiresAt: getJWTTime(ATokenExpiredDuration),
Issuer: TokenIssuer,
}
at := MyClaim{
UserID,
Username,
rc,
}
atoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, at).SignedString(mySecret)
// refresh token 不需要保存任何用户信息
rt := rc
rt.ExpiresAt = getJWTTime(RTokenExpiredDuration)
rtoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, rt).SignedString(mySecret)
return
}
在验证用户登录之后,根据传入的UserID 和Username ,生成atoken和rtoken。在颁发token中可以分别规定两个token的过期时间
校验token
// VerifyToken 验证Token
func VerifyToken(tokenID string) (*MyClaim, error) {
var myc = new(MyClaim)
token, err := jwt.ParseWithClaims(tokenID, myc, keyFunc)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, ErrorInvalidToken
}
return myc, nil
}
根据传入的token值来判断是否有错误,如果错误为无效,说明token格式不正确或者token已经过期。
无感刷新token
首先校验rtoken是否还有效,rtoken有效的情况下继续校验atoken是否是因为过期导致的失效,是的话可以刷新atoken然后返回给前端。
// RefreshToken 通过 refresh token 刷新 atoken
func RefreshToken(atoken, rtoken string) (newAtoken, newRtoken string, err error) {
// rtoken 无效直接返回
if _, err = jwt.Parse(rtoken, keyFunc); err != nil {
return
}
// 从旧access token 中解析出claims数据
var claim MyClaim
_, err = jwt.ParseWithClaims(atoken, &claim, keyFunc)
// 判断错误是不是因为access token 正常过期导致的
v, _ := err.(*jwt.ValidationError)
if v.Errors == jwt.ValidationErrorExpired {
return GenToken(claim.UserID, claim.Username)
}
return
}
完整代码
package main
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v4"
)
const (
ATokenExpiredDuration = 10 * time.Minute
RTokenExpiredDuration = 30 * 24 * time.Hour
TokenIssuer = ""
)
var (
mySecret = []byte("xxxx")
ErrorInvalidToken = errors.New("verify Token Failed")
)
type MyClaim struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func getJWTTime(t time.Duration) *jwt.NumericDate {
return jwt.NewNumericDate(time.Now().Add(t))
}
func keyFunc(token *jwt.Token) (interface{}, error) {
return mySecret, nil
}
// GenToken 颁发token access token 和 refresh token
func GenToken(UserID int64, Username string) (atoken, rtoken string, err error) {
rc := jwt.RegisteredClaims{
ExpiresAt: getJWTTime(ATokenExpiredDuration),
Issuer: TokenIssuer,
}
at := MyClaim{
UserID,
Username,
rc,
}
atoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, at).SignedString(mySecret)
// refresh token 不需要保存任何用户信息
rt := rc
rt.ExpiresAt = getJWTTime(RTokenExpiredDuration)
rtoken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, rt).SignedString(mySecret)
return
}
// VerifyToken 验证Token
func VerifyToken(tokenID string) (*MyClaim, error) {
var myc = new(MyClaim)
token, err := jwt.ParseWithClaims(tokenID, myc, keyFunc)
if err != nil {
return nil, err
}
if !token.Valid {
err = ErrorInvalidToken
return nil, err
}
return myc, nil
}
// RefreshToken 通过 refresh token 刷新 atoken
func RefreshToken(atoken, rtoken string) (newAtoken, newRtoken string, err error) {
// rtoken 无效直接返回
if _, err = jwt.Parse(rtoken, keyFunc); err != nil {
return
}
// 从旧access token 中解析出claims数据
var claim MyClaim
_, err = jwt.ParseWithClaims(atoken, &claim, keyFunc)
// 判断错误是不是因为access token 正常过期导致的
v, _ := err.(*jwt.ValidationError)
if v.Errors == jwt.ValidationErrorExpired {
return GenToken(claim.UserID, claim.Username)
}
return
}
二、双Token最佳实践
使用 JWT 实现的双令牌验证主要有以下几步:

后端需要对外提供一个刷新Token的接口,前端需要是实现一个当Access Token过期时自动请求刷新Token接口获取新Access Token的拦截器。- 用户登录时,服务端同时生成并返回
access token和refresh token。其中,access token的有效期较短,例如10分钟。而refresh token的有效期较长,例如30天。这两种token都可以用jwt-go生成。 - 如果
refresh token也过期了,那么客户端必须要重新登录,以获取新的access token和refresh token。
注意:在实际的生产环境中,为了保证系统的安全性,你可能需要考虑到以下几点:
Token也可以在服务端保存一份,比如存到Redis中,并对前端传来的token与redis中的比较,这样可以实现服务端主动让token失效,比如从redis删除token即可。- 考虑到用户的
session状态,当用户退出登录或者修改密码后,需要把保存在服务端的refresh token删除或者置为无效。 - 应用
HTTPS协议以保护你的token不被截获。 - 使用黑名单机制,当用户的
token被盗或者用户退出登录后,你可以把这个token添加到黑名单中,防止它再次被用于请求。 - 考虑到服务的可用性,你可能需要把
token保存在像Redis这样的内存数据库中,以提升性能。
以上是 JWT 双令牌验证的一种常见的最佳实践,但需要注意,不同的业务场景可能需要不同的安全策略,总是需要根据实际业务需求和环境来灵活调整。
三、SSO单点登录
SSO说明
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。https://baike.baidu.com/item/SSO/3451380
例如访问在网易账号中心(http://reg.163.com/ )登录之,访问以下站点都是登录状态
- 网易直播 http://v.163.com
- 网易博客 http://blog.163.com
- 网易花田 http://love.163.com
- 网易考拉 https://www.kaola.com
- 网易Lofter http://www.lofter.com
SSO设计可参考:单点登录(SSO)的设计与实现
6358






