上文已经介绍了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)的设计与实现