⽤户认证
⽤户认证
HTTP 是⼀个⽆状态的协议,一次请求结束后,下次在发送服务器就不知道这个请求是谁发来的了(同一个 IP 不代表同一个⽤户),在 Web 应用中,用户的认证和鉴权是⾮常重要的一环,实践中有多种可用⽅案,并且不同的方案各有千秋。
Cookie- Session 认证模式
在 Web 应用发展的初期,⼤部分网站采用基于 Cookie-Session 的会话管理⽅式,逻辑如下。
- 客户端输入⽤户名、密码进⾏认证
- 服务端验证⽤户名、密码正确后生成并存储 Session,将 SessionID 通过 Cookie 返回给客户端
- 客户端访问需要认证的接口时在 Cookie 中携带 SessionID
- 服务端通过 SessionID 查找 Session 并进⾏鉴权,返回给客户端需要的数据

基于 Session 的⽅式存在多种问题。
- 服务端需要存储 Session,并且由于 Session 需要经常快速查找,通常存储在内存或内存数据库 中,同时在线用户较多时需要占用⼤量的服务器资源。
- 当需要扩展时,创建 Session 的服务器可能不是验证 Session 的服务器,所以还需要将所有 Session 单独存储并共享。
- 由于客户端使⽤ Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种⽅式也难以防范 CSRF 攻击。
Token 认证模式
鉴于基于 Session 的会话管理⽅式存在上述多个缺点,基于 Token 的⽆状态会话管理方式诞⽣了,所谓⽆状态,就是服务端可以不再存储信息,甚⾄是不再存储 Session,逻辑如下。
- 客户端使⽤用户名、密码进行认证
- 服务端验证⽤户名、密码正确后⽣成 Token 返回给客户端
- 客户端保存 Token,访问需要认证的接⼝时在 URL 参数或 HTTP Header 中加⼊ Token
- 服务端通过解码 Token 进行鉴权,鉴权通过后返回给客户端需要的数据
基于 Token 的会话管理⽅式有效解决了基于 Session 的会话管理存在的问题。
- 服务端不需要存储和⽤户鉴权有关的信息,鉴权信息会被加密到 Token 中,服务端只需要读取 Token 中包含的鉴权信息即可
- 避免了共享 Session 导致的系统不易扩展问题
- 不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
- 使⽤ CORS 可以快速解决跨域问题
JWT介绍
JWT 是 JSON Web Token 的缩写,是为了在⽹络应用环境间传递声明⽽流⾏的⼀种基于 JSON 的开放标准(RFC 7519)。JWT 本身没有定义任何技术实现,它只是定义了一种基于 Token 的会话管理的规则,涵盖 Token 需要包含的标准内容和 Token 的⽣成过程,特别适⽤于分布式站点的单点登录(SSO)场景。
一个 JWT Token 就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjE5MjYxOTkwNTg1NTMxMTQ2MjQsIlVzZXJuYW1lIjoiUTFtaSIsImlzcyI6Imxpd2VuemhvdS5jb20iLCJzdWIiOiJkZW1vIiwiZXhwIjoxNzQ4MjI4NTEzfQ.NHFkKk2e87CYDqp3NmDEzI6UNAg5K6Tv3IuyDf8_jlw它是由 . 分隔的三部分组成,这三部分依次是:
- 头部(Header)
- 负载(Payload)
- 签名(Signature)
头部和负载以 JSON 形式存在,这就是 JWT 中的 JSON,三部分的内容都分别单独经过了 Base64 编码,以 . 拼接成⼀个 JWT Token。

Header
JWT 的 Header 中存储了了所使⽤用的加密算法和 Token 类型。
{
"alg": "HS256",
"typ": "JWT"
}Payload
Payload 表示负载,也是一个 JSON 对象,JWT 规定了 7 个官方字段供选⽤,
{
iss (issuer):签发⼈
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):⽣效时间
iat (Issued At):签发时间
jti (JWT ID):编号
}除了官⽅字段,开发者也可以⾃己指定字段和内容,例如下面的内容。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}注意,JWT 默认是不加密的,任何人都可以读到,所以不要把机密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防⽌数据篡改。
首先,需要指定⼀个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使⽤ Header ⾥面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产⽣签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)JWT优缺点
JWT 拥有基于 Token 的会话管理⽅式所拥有的一切优势,不依赖 Cookie,使得其可以防止 CSRF 攻击,也能在禁用 Cookie 的浏览器环境中正常运行。
而 JWT 的最⼤优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以⽅方便便扩展,避免存储Session 所需要引入的 Redis 等组件,降低了系统架构复杂度。
但这也是 JWT 最大的劣势,由于有效期存储在 Token 中,JWT Token ⼀旦签发,就会在有效期内⼀直可用,⽆法在服务端废止,当⽤户进行登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁⽤用户,单纯使用 JWT 就⽆法做到了。(可以前置增加一个黑名单过滤,在黑名单中的 token 直接拒绝)
基于 jwt 实现认证实践
前⾯讲的 Token,都是 Access Token,也就是访问资源接口时所需要的 Token,还有另外⼀种 Token:Refresh Token,通常情况下,Refresh Token 的有效期会比较长,而 Access Token 的有效期⽐较短,当 Access Token 由于过期⽽失效时,可以使用 Refresh Token 就可以获取到新的 Access Token,如果 Refresh Token 也失效了,⽤户就只能重新登录了。
在 JWT 的实践中,引入 Refresh Token 后的会话管理的流程改进如下。
- 客户端使⽤用户名、密码进⾏认证
- 服务端⽣成有效时间较短的 Access Token(例如 10 分钟)和有效时间较长的 Refresh Token(例如7天)
- 客户端访问需要认证的接⼝时,携带 Access Token
- 如果 Access Token 没有过期,服务端鉴权通过后返回给客户端需要的数据
- 如果携带 Access Token 访问需要认证的接⼝时,返回鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
- 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token,客户端使⽤新的 Access Token 访问需要认证的接⼝

后端需要对外提供⼀个刷新 Token 的接口,前端需要实现⼀个当 Access Token 过期时⾃动请求刷新 Token 接口获取新 Access Token的拦截器。
Go 语言 Jwt 库
下载依赖
go get -u github.com/golang-jwt/jwt/v5引入依赖
import "github.com/golang-jwt/jwt/v5"创建 Token
mySigningKey := []byte("AllYourBase")
type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
// Create claims with multiple fields populated
claims := MyCustomClaims{
"bar",
jwt.RegisteredClaims{
// A usual scenario is to set the expiration time relative to the current time
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
Audience: []string{"somebody_else"},
},
}
fmt.Printf("foo: %v\n", claims.Foo)
// Create claims while leaving out some of the optional fields
claims = MyCustomClaims{
"bar",
jwt.RegisteredClaims{
// Also fixed dates can be used for the NumericDate
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Println(ss, err)解析 Token
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil
})
if err != nil {
log.Fatal(err)
} else if claims, ok := token.Claims.(*MyCustomClaims); ok {
fmt.Println(claims.Foo, claims.RegisteredClaims.Issuer)
} else {
log.Fatal("unknown claims type, cannot proceed")
}