Token:如何降低用户身份鉴权的流量压力?

网络 网络管理
用户中心需要维护大量的 Session 缓存,并且频繁被各个业务系统访问。如果缓存出现故障,所有依赖它的子系统将无法进行用户身份确认,导致服务中断。这主要是由于 Session 缓存与各子系统的高耦合。

许多网站在初期阶段通常使用 Session 方式来实现用户登录鉴权。具体而言,当用户成功登录后,服务端会将用户的相关信息存储在 Session 缓存 中,并生成一个唯一的 session_id,这个 ID 被存储在用户的 Cookie 中。之后,用户每次发送请求时,都会携带该 session_id,服务端则通过该 ID 查找到 Session 缓存中的用户记录,从而进行身份验证和用户信息的管理。

这种用户鉴权方式的优势在于,所有用户信息都存储在服务端,不会暴露任何敏感数据给客户端,同时每个登录用户都有共享的 Session 缓存空间。但是,随着网站流量的增长,这种设计也会暴露出明显的缺点——用户中心的身份鉴权在高并发下表现不稳定。

具体而言,用户中心需要维护大量的 Session 缓存,并且频繁被各个业务系统访问。如果缓存出现故障,所有依赖它的子系统将无法进行用户身份确认,导致服务中断。这主要是由于 Session 缓存与各子系统的高耦合。每次请求都至少需要访问一次缓存,因此缓存的容量和响应速度直接影响了全站的 QPS 上限,降低了系统的隔离性,使各子系统之间互相影响。

那么,如何降低用户中心与各子系统之间的耦合度,从而提高系统性能呢?接下来我们一起来探讨。

JWT 登陆和 token 校验

常见方式是采用签名加密的 token,这是登录的一个行业标准,即 JWT(JSON Web Token):

图片图片

上图就是 JWT 的登陆流程,用户登录后会将用户信息放到一个加密签名的 token 中,每次请求都把这个串放到 header 或 cookie 内带到服务端,服务端直接将这个 token 解开即可直接获取到用户的信息,无需和用户中心做任何交互请求。

token 生成代码如下:

import "github.com/dgrijalva/jwt-go"


//签名所需混淆密钥 不要太简单 容易被破解
//也可以使用非对称加密,这样可以在客户端用公钥验签
var secretString = []byte("jwt secret string 137 rick") 


type TokenPayLoad struct {
    UserId   uint64 `json:"userId"` //用户id
    NickName string `json:"nickname"` //昵称
    jwt.StandardClaims //私有部分
}


// 生成JWT token
func GenToken(userId uint64, nickname string) (string, error) {
    c := TokenPayLoad{
        UserId: userId, //uid
        NickName: nickname, //昵称
//这里可以追加一些其他加密的数据进来
//不要明文放敏感信息,如果需要放,必须再加密


//私有部分
        StandardClaims: jwt.StandardClaims{
//两小时后失效
            ExpiresAt: time.Now().Add(2 * time.Hour).Unix(),
//颁发者
            Issuer:    "geekbang",
        },
    }
//创建签名 使用hs256
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 签名,获取token结果
return token.SignedString(secretString)
}

可以看出,这种 Token 内部包含了过期时间,接近过期的 Token 会在客户端自动与服务端通信进行更新。这样设计可以大大增加恶意截取客户端 Token 并伪造用户身份的难度。同时,服务端还可以实现与用户中心的解耦,业务服务端只需解析请求中的 Token 就能获取用户信息,而不必每次请求都去访问用户中心。Token 的刷新完全可以由客户端主动向用户中心发起,而无需业务服务端频繁请求用户中心来更换 Token。

那么,JWT(JSON Web Token)是如何保证数据不会被篡改并确保数据完整性的呢?接下来我们来看看它的组成。

图片图片

JWT token 解密后的数据结构如下图所示:

//header
//加密头
{
"alg": "HS256", // 加密算法,注意检测个别攻击会在这里设置为none绕过签名
"typ": "JWT" //协议类型
}


//PAYLOAD
//负载部分,存在JWT标准字段及我们自定义的数据字段
{
"userid": "9527", //我们放的一些明文信息,如果涉及敏感信息,建议再次加密
"nickname": "Rick.Xu", // 我们放的一些明文信息,如果涉及隐私,建议再次加密
"iss": "geekbang",
"iat": 1516239022, //token发放时间
"exp": 1516246222, //token过期时间
}


//签名
//签名用于鉴定上两段内容是否被篡改,如果篡改那么签名会发生变化
//校验时会对不上

JWT 如何验证 token 是否有效,还有 token 是否过期、是否合法,具体方法如下:

func DecodeToken(token string) (*TokenPayLoad, error) {
    token, err := jwt.ParseWithClaims(token, &TokenPayLoad{}, func(tk *jwt.Token) (interface{}, error) {
return secret, nil
    })
if err != nil {
return nil, err
    }
if decodeToken, ok := token.Claims.(*TokenPayLoad); ok && token.Valid {
return decodeToken, nil
    }
return nil, errors.New("token wrong")
}

JWT(JSON Web Token)的解码相对简单,第一部分和第二部分都是通过 Base64 编码的。解码这两部分即可获取到 payload 中的所有数据,其中包括用户昵称、UID、用户权限和 Token 的过期时间。要验证 Token 是否过期,只需将其中的过期时间与当前时间进行对比,即可确认 Token 是否有效。而验证 Token 的合法性则通过 签名验证来完成。任何对信息的修改都无法通过签名验证。如果 Token 通过了签名验证,就表明它没有被篡改过,是一个合法的 Token,可以直接使用。

这个过程如下图所示:

图片图片

图片通过 Token 方式,可以显著减轻用户中心的压力,不再需要频繁访问用户信息接口。各业务服务端只需解码并验证 Token 的合法性,即可直接获取用户信息。然而,这种方式也存在一些缺点。比如,当用户被拉黑后,客户端通常要等到 Token 过期才会自动登出,这会导致管理上的一定延迟。

如果希望实现实时管理,可以在服务端暂存新生成的 Token,并在每次用户请求时与缓存中的 Token 进行对比。不过,这样的操作会影响系统性能,因此少数公司会采用这种方式。为了提高 JWT 系统的安全性,Token 通常设置较短的过期时间,通常为十五分钟左右。Token 过期后,客户端会自动向服务端请求更新。

token 的更换和离线

那么如何对 JWT 的 token 进行更换和离线验签呢?具体的服务端换签很简单,只要客户端检测到当前的 token 快过期了,就主动请求用户中心更换 token 接口,重新生成一个离当前还有十五分钟超时的 token。但是期间如果超过十五分钟还没换到,就会导致客户端登录失败。为了减少这类问题,同时保证客户端长时间离线仍能正常工作,行业内普遍使用双 token 方式,具体你可以看看后面的流程图:

图片图片

图片

在这个方案中,使用了两种 Token:

  1. Refresh Token:用于更换 Access Token,有效期为 30 天。
  2. Access Token:用于存储当前用户信息和权限信息,每隔 15 分钟进行一次更换。

当客户端尝试请求用户中心进行 Token 更换但失败,且客户端处于离线状态时,只要本地的 Refresh Token 未过期,系统仍然能够正常运作。客户端可以持续使用 Access Token,直到 Refresh Token 到期,此时系统会提示用户重新登录。通过这种方式,即便用户中心出现故障,业务系统也可以正常运转一段时间,提升了系统的健壮性和用户体验。

用户中心检测更换 token 的实现如下:

//如果还有五分钟token要过期,那么换token
if decodeToken.StandardClaims.ExpiresAt < TimestampNow() - 300 {
//请求下用户中心,问问这个人禁登陆没
//....略具体


//重新发放token
  token, err := GenToken(.....)
if err != nil {
return nil, err
  }
//更新返回cookie中token
  resp.setCookie("xxxx", token)
}

安全建议

在使用 JWT 方案时,除了代码注释中提到的内容外,还有一些关键注意事项值得留意:

  1. 确保通讯安全:使用 HTTPS 协议传输数据,以降低 Token 被拦截的风险。
  2. 限制 Token 的更换频率:要控制 Token 的更换次数,并定期刷新 Token。例如,限制用户的 Access Token 每天只能更换 50 次,如果超出次数则要求用户重新登录,同时每 15 分钟更换一次 Token。这样可以减少 Token 被盗后的潜在影响。
  3. 安全存储 Web Token:对于 Web 用户,当 Token 存储在 Cookie 中时,建议设置 HttpOnlySameSite=Strict 标记,以防止 Cookie 被恶意脚本窃取。
责任编辑:武晓燕 来源: 二进制跳动
相关推荐

2024-10-10 12:21:56

JWTSession扩展性

2021-03-03 13:25:35

CookieSessionToken

2021-09-01 10:15:15

前端cookiesession

2024-10-29 09:40:07

流量技术架构

2019-05-20 14:57:35

Tomcat容器安全

2024-01-26 14:35:03

鉴权K8sNode

2014-07-10 11:34:05

2010-03-26 13:55:44

2010-10-26 11:22:26

2021-09-02 07:00:32

鉴权Web 应用Cookie-sess

2022-12-05 15:02:14

鸿蒙用户鉴权

2018-01-10 14:22:05

2024-05-28 08:24:18

2021-05-27 07:12:19

单点登录系统

2022-12-02 16:28:47

2018-09-27 14:37:09

风险云计算安全

2022-05-31 08:36:41

微服务网关鉴权

2021-10-26 11:42:51

系统

2023-04-17 08:56:29

微服务鉴权业务

2023-03-30 07:48:46

接口鉴权SpringBoot
点赞
收藏

51CTO技术栈公众号