golang服务器后端服务:JWT+Redis实现用户登录验证

用户登录校验成功后:

  • 创建一条aes加密数据,目前仅用作验证,如下:
    • aes加密所使用的密钥随机生成,这里称之为:signKey,保存在token中传给前端;
    • 使用密钥signKey将该用户的ID加密,得到一条密文,这里称之为:hashData,保存在redis中;
  • 在redis中存储一条数据,并设定过期时间(到期之间内未操作,自动删除),如下:
    • 1、该条redis数据的key值,随机生成,这里称之为:rdsKey,保存在token中传给前端;
    • 2、该条redis数据的value值,如下:
      • 2.1、用户信息,将数据库中的用户信息保存到redis中;
      • 2.2、aes加密数据 hashData
      • 2.3、其他数据,如:产品ID之类的;
  • 创建一条token数据,发送给前端,如下:
    • 1、aes加密数据的密钥,signKey
    • 2、redis数据的key值 rdsKey
    • 3、该用户的ID(只包含用户ID)

登录后用户其他操作:

前端需要将保存的token传给后端。

  • 后端解析token可以得到 用户ID、rdsKey 以及 signKey 三样数据,然后开始操作:
    • 1、通过 rdsKey 查询redis数据,先开始校验:
      • 1.1、如果没找到,原因可能有如下三种:
        • 1.1.1、用户未登录;
        • 1.1.2、登录超时(redis中的数据到期自动删除);
        • 1.1.3、前端存储的 token 中的 rdsKey 被非法修改;
        • 1.1.4、该情况直接返回错误信息通知前端让用户重新登录;
      • 1.2、如果找到,通过token中的signKey,解密redis中的hashData数据,对于解密是否成功的处理如下:
        • 1.2.1、解密失败:可能是前端token中的signKey被修改,或者解密程序出错,返回错误信息,通知前端让客户重新登录;
        • 1.2.2、解密成功:可以得到hashData解密后的用户ID明文,比对token中的用户ID是否同解密后的用户ID一致。如果不一致,说明前端的token数据绝对是被修改过的,可以执行一些对用户的惩罚方法,如禁止登陆、查封账号之类的;
    • 2、token校验通过,用户的信息数据、产品id都可以直接在redis中拿到,就不用进行数据库操作了,减少服务器的压力;
    • 3、需要更新redis中该条数据的有效期,若不更新的话,该条数据会在第一次设定的时间到期后自动删除;
  • 解决了前端数据不安全的问题,因为该方案内前端保存的token并没有任何隐私、和涉及安全性操作的数据。
  • 有效的保证后端验证安全性。如果前端的token数据中任何值被修改,都会导致后端验证失败,从而迫使前端用户重新登录。
  • 利用redis高速读写性能,缓存已登录的用户信息在服务器的redis中,有效的减少数据库的操作。
  • 利用redis数据的有效期特性,解决了token令牌过期作废的问题,只要在redis中查询不到token中的 rdsKey ,就能判断该token为废令牌;

后期拓展:

  • 后期可在redis中存储权限map,校验登录完成后,可立即开始校验用户权限。

具体代码:

后端使用的是 beego api ,作为一个Restful风格的服务端,还支持自动生成swagger风格文档。

先增加登录成功后的代码

在controller的用户登录函数中:

	// todo:
	// 1、前置的操作,比如用户信息从数据库查询,比对密码之类的,太多例子,这里就不写了。
	// 2、总之,用户登录信息验证通过后,开始执行如下:
	
	// 创建一条aes加密数据
	signKey := sid.New() //随机生成即可,可以使用rand之类的生成一个符合AES CBC模式加密的密钥就行
	ok, hashData := utils.AesEncryptCBC([]byte(user.Id), []byte(signKey))
	// utils.AesEncryptCBC() 方法就是执行加密操作。
	// 之前文章有所介绍:https://blog.csdn.net/mirage003/article/details/87868999
	if !ok {
		return reData.msg(ErrServerError)
		// 这里是返回给前端一条类似于内部错误的消息,说明加密的时候出错,不能进行下一步。
	}
	
	// 在redis中存储一条数据,并设定过期时间(到期之间内未操作,自动删除)
	rdsVal := make(map[string]interface{}, 3)
	rdsVal["user"] = user         //用户信息,将数据库中的用户信息保存到redis中;
	rdsVal["hashData"] = base64.StdEncoding.EncodeToString(hashData) //aes加密数据 hashData;
	//rdsVal["prodId"] =  prod.Id //当前产品的id
	ok, rdsKey := rds.PutJSON("", "user", rdsVal, 1800*time.Second) 
	// redis存储,并设定1800秒后数据到期
	// beego中如何使用redis可以参考:https://beego.me/docs/module/cache.md
	if !ok {
		return reData.msg(ErrServerError)
		// 这里是返回给前端一条类似于内部错误的消息,说明redis存储失败,不能进行下一步。
	}

	// 创建一条token数据
	tokenData := make(map[string]interface{}, 3)
	tokenData["id"] = user.Id
	tokenData["signKey"] = signKey
	tokenData["rdsKey"] = rdsKey
	ok, tokenStr := utils.GetHStoken(&tokenData, conf.SecretKey) 
	// 生成HS256算法的token
	// 网上太多例子,这里不再写了。
	if !ok {
		return reData.msg(ErrServerError)
		// 这里是返回给前端一条类似于内部错误的消息,说明token生成失败,不能进行下一步。
	}

	// 最后,将token传给前端
	data := make(map[string]string, 1)
	data["token"] = tokenStr
	return reData.msg(OK).res(true).data(data)

然后增加一个路由过滤器 filter

beego中很方便就能添加,参考文档:https://beego.me/docs/mvc/controller/filter.md

接着在filter中实现验证和读取:

func filter() {
	var FilterBase = func(ctx *context.Context) {
		urlPath := ctx.Request.URL.Path
		if urlPath == "/v1/user/login" { //登录页面不检查
			return
		}
		
		// 获取token
		var token string
		if token = ctx.Request.Header.Get("YS-Token"); token == "" { //先从header里面取出token
			// 如果没取到,说明用户未登录
			result(ctx,"50008","请登录") // 返回消息通知前端让用户进行登录
			return
		}
		
		// 解析 token
		ok, data := utils.ParseHStoken(token, conf.SecretKey)
		if !ok { //如果token解析失败
			result(ctx,"50008","令牌验证失败,请重新登录。") // 返回消息通知前端让用户进行登录
			return
		}
		rdsKey := jsoni.GetStr(*data, "rdsKey")   // 从token中获取 rdsKey
		signKey := jsoni.GetStr(*data, "signKey") // 从token中获取 signKey
		tUID := jsoni.GetStr(*data, "id")         // 从token中获取 用户id
		// jsoni是我自己封装的一个函数,
		// 调用的是  github.com/json-iterator/go 这个第三方库,
		// 运行效率比自带的快很多(据说golang 1.10+版本以上,两者差距不大了。所以请自行斟酌。)
		
		// 开始对用户进行登录验证
		code, rdsData := rds.GetBytes(rdsKey)     //查询redis
		if code == 0 { // redis查询返回0,代表出错(我自己定义的一个方法)
			result(ctx,"50008","服务器发生错误,请重试。")
			return
		}else if code==1{ // redis查询返回1,代表未找到(我自己定义的一个方法)
			result(ctx,"50008","登录超时,请重新登录。")
			return
		}
		hashData := jsoni.GetStr(*rdsData, "hashData")          // 从rdsData中获取 hashData
		bytes, err := base64.StdEncoding.DecodeString(hashData) // hashData转为[]byte
		if err != nil {
			beego.Error("routers.filter.FilterBase: ", err)
			result(ctx,"50008","服务器发生错误,请重试。")
			return
		}
		// 验证aes加密
		if ok, decrypted := utils.AesDecryptCBC(bytes, []byte(signKey)); !ok {
			result(ctx,"50008","服务器发生错误,请重试。")
			return
		} else {
			if string(decrypted) != tUID {
				result(ctx,"50008","验证失败")
				return
			}
		}
		
		// 执行到这里,说明用户登录验证已经完成,开始将用户信息提取出来
		// 这里我将用户信息加到header中,在controller函数内就可以直接取到,不用再次查询redis了
		userData := jsoni.GetStr(*rdsData, "user")
		if userData == "" { //如果没有获取到用户信息,这里只是一个容错处理,redis中获取用户信息失败的概率很小
			// 将用户的id传过去
			ctx.Request.Header.Add("userData", `{id:"`+tUID+`"}`)
		} else { //否则,直接传入用户的全部信息
			ctx.Request.Header.Add("userData", userData)
		}
	}
	beego.InsertFilter("/*", beego.BeforeRouter, FilterBase)	
}

最后,在controller用户登出控制函数中,增加一个删除redis数据的方法即可。(当然,不删除也行,该数据到期以后,redis会自动清除)

运行效果:

1、登录:
golang服务器后端服务:JWT+Redis实现用户登录验证_第1张图片
2、登陆后的其他操作验证:
golang服务器后端服务:JWT+Redis实现用户登录验证_第2张图片
校验成功,正常进入controller对应的函数内 ↑

golang服务器后端服务:JWT+Redis实现用户登录验证_第3张图片
未传入token,提示请登录 ↑

golang服务器后端服务:JWT+Redis实现用户登录验证_第4张图片
token数据被修改,提示重新登录↑

你可能感兴趣的:(golang)