背景: 在开发设计中涉及到JWT的校验问题,但是发现一个漏洞同时也是JWT设计的理念,那就是后端不进行相关的Token存储,后端只进行相关的Token验证,同时还有就是token的生成和刷新,那么问题来了,那就是后端校验不会知道是否是最新的token
也就是说,可能已经刷新过了token,但是发现旧的token还没过期,这个时候还可以进行使用,在我看来还是有安全风险的,我想要的设计是用户退出登录或者刷新Token后,旧的token要立即失效,这样才够安全,不然一旦被黑客拿到旧的token,那么它就可以利用旧的token一直刷新使用,
后果很严重。
设计的理念: 添加Redis作为黑名单进行绑定,旧的Token,设计理念借鉴了下面博客:
下面是我设计的具体的流程图:(不过我没有考虑旧的token过期时间问题,所以也可以进行对存储在Redis中的数据进行相关的时间设定,也就是旧的token过期时间到了,redis中的数据自动清除,这样也可以对Redis进行相关的维护,不然时间越长Redis的压力越大。)
具体的实现:(login登录就不做处理了,因为就是正常的获取Token的业务,很简单,如果没有基础可以参考我之前的博客)
主函数注册API接口并启动服务器
func main() {
r := gin.Default()
r.GET("/login", login)
r.GET("/refresh", refresh)
r.GET("/sayHello", sayHello)
r.Run(":9090")
//测试URL实例
//http://localhost:9090/login?username=dong&password=123456
//http://localhost:9090/verify?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyMTAsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Esh1Zge0vO1BAW1GeR5wurWP3H1jUIaMf3tcSaUwkzA
//http://localhost:9090/refresh?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTIyNDMsImlhdCI6MTU2MDUwODYxMCwidXNlcl9pZCI6MSwicGFzc3dvcmQiOiIxMjM0NTYiLCJ1c2VybmFtZSI6ImRvbmciLCJmdWxsX25hbWUiOiJkb25nIiwicGVybWlzc2lvbnMiOltdfQ.Xkb_J8MWXkwGUcBF9bpp2Ccxp8nFPtRzFzOBeboHmg0
}
以刷新token,让旧Token失效的方式(退出登录的方式相同,这里面对redis存储的数据进行了时间的处理,也就是会根据过期时间将数据彻底清空掉)
/**
* @Description: 功能描述(刷新JWT的token)
* @Date : 2020/11/12
*/
func refresh(c *gin.Context) {
strToken := c.Query("token")
claims, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
// 获取失效时间,填充到Redis中。
a := int(claims.ExpiresAt - time.Now().Unix()) //剩余过期时间
utils.Set(strToken, "1", a) //存在Redis中的数据,对应的过期时间为:a表示秒
claims.ExpiresAt = time.Now().Unix() + (claims.ExpiresAt - claims.IssuedAt) // 失效的时间
signedToken, err := getToken(claims)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
c.String(http.StatusOK, signedToken)
}
具体的业务实现:(会进行判断Redis中的是否存在该数据,如果不存在则表明没有加入黑名单,否则就是加入了黑名单)
/**
* @Description: 功能描述(业务测试,用于真正的开发业务验证是否用户合法,然后执行Hello业务)
* @Date : 2020/11/12
*/
func sayHello(c *gin.Context) {
strToken := c.Query("token")
fmt.Println("token=", strToken)
flag := utils.GetStringValue(strToken)
fmt.Println("flag =", flag)
if flag == "1" {
c.JSON(http.StatusBadRequest, errors.New(ErrorReason_ReLogin))
c.Abort()
return
}
claim, err := verifyAction(strToken)
if err != nil {
c.String(http.StatusNotFound, err.Error())
return
}
if claim.Role == "admin" {
fmt.Println("该用户是管理员可以执行相关的业务")
fmt.Println("--------------执行Hello业务--------------")
}
c.JSON(http.StatusOK, claim)
}