身份认证与授权方案
❝
可以遗憾,但不要后悔。
我们留在这里,从来不是身不由己。
——— 而是选择在这里经历生活
❞
概述
-
JWT、Token + Redis和SSO(OAuth2)都是常见的身份验证和授权方案,它们都有自己的优缺点和适用场景,下面对它们进行比较。 -
需要注意的是,这三种方案并不是完全互斥的,而是可以互相补充和组合。在实际应用中,应根据具体的场景和需求,选择最适合自己的方案。
常见方案对比
JWT
使用 JWT,可以在一定程度上替代 Redis 的某些使用场景。Redis 通常用于存储会话信息、缓存数据、消息队列等,而 JWT 可以用于传递会话信息、授权信息等,因此可以避免一些常规的 Redis 使用场景。
具体而言,使用 JWT 可以实现无状态(stateless)的身份验证和授权,不需要在服务器端存储会话信息,也不需要每次请求都从 Redis 中获取数据。这样可以减轻服务器的负担,提高系统的性能和可扩展性。同时,JWT 还可以通过设置过期时间和刷新令牌等方式,实现会话管理和安全性控制,进一步简化了服务器端的逻辑。
用户基本信息可以直接放在 JWT 的 payload 中,注意敏感信息不要放在这里面。如 username,nickname,role、email、empid 等。
| 对比 | 说明 |
|---|---|
| 优点 | JWT 的最大优点在于它的无状态性,它不需要在服务器端存储会话信息,可以轻松地实现分布式和高可扩展性。同时,JWT 还可以包含任意数量的声明,可以根据需要灵活地扩展和定制,使用方便。 |
| 缺点 | JWT 的缺点在于它的签名机制,并不能实现完全的加密保护。同时,如果要撤销 JWT,就必须等待其过期,无法实现即时失效。 |
适用场景:去中心化,适合于无状态的 Web 应用,特别是移动端应用,可以用于实现单点登录和分布式系统的身份验证和授权。
Token + Redis
| 对比 | 说明 |
|---|---|
| 优点 | Token + Redis 的优点在于它的即时失效性,可以立即撤销令牌,提高了安全性。同时,它可以使用 Redis 存储会话信息,支持大规模分布式系统。 |
| 缺点 | Token + Redis 的缺点在于它的可扩展性,如果需要增加服务器节点,就必须同步会话信息,增加了开发和维护成本。 |
适用场景:适合于对安全性要求较高、会话管理较为复杂的 Web 应用,特别是 B2B 系统或企业内部系统。
SSO(OAuth2)
| 对比 | 说明 |
|---|---|
| 优点 | SSO 的优点在于它的灵活性和安全性,可以支持多种身份验证和授权方案,包括本地身份验证、社交账号登录、企业身份认证等,提高了用户的使用体验和安全性。同时,SSO 还可以支持单点登录,避免用户重复登录多个应用。 |
| 缺点 | SSO 的缺点在于它的开发和维护成本较高,需要处理多个应用之间的信任关系、权限分配等复杂的问题。同时,如果系统遭受攻击,可能会影响多个应用的安全性。 |
适用场景:适合于大型企业内部系统、社交平台等多应用场景,要求支持多种身份验证和授权方案,提供单点登录功能,同时有较高的安全性和稳定性要求。
JWT 原理
介绍
JWT(JSON Web Token) 是一种用于在网络中传输声明 (claims) 的开放标准 (RFC 7519),它可以在客户端和服务器之间安全地传递信息,以便验证和识别用户。
可以使用 JWT 官网 Debugger 功能进行验证真伪。
需要注意的是,JWT 仅提供了数据的完整性和可靠性,但并没有提供数据的加密性。如果需要保护数据的隐私性,应该使用加密算法对数据进行加密。另外,由于 JWT 本身是基于 HTTP 协议的,因此也需要考虑一些常见的安全风险,例如 XSS、CSRF 等。
组成部分
JWT 由三部分组成:头部 (header)、载荷 (payload) 和签名 (signature):
- 头部:描述了生成的
JWT的元数据,通常包括两个属性:算法(alg)和类型(typ),这里使用的是JSON格式。 - 载荷:包含了需要传递的信息(称为声明),可以包含多个声明,通常包括用户身份信息、访问权限、过期时间等,这里使用的是
JSON格式。 - 签名:用于验证
JWT的完整性和可靠性,通常使用一个密钥(secret)进行签名,并将头部和载荷的内容与密钥一起进行哈希计算,以生成签名。
验证流程
JWT 的生成和验证流程如下:
- 服务器生成
JWT,使用算法和密钥对头部和载荷进行签名,将签名和JWT发送给客户端。 - 客户端将
JWT存储在本地,并在请求服务器时将其作为Authorization头部或Cookie发送给服务器。 - 服务器接收到请求,从
Authorization头部或Cookie中获取JWT,并对其进行解密,验证签名和过期时间。 - 如果
JWT验证通过,则从载荷中获取用户身份信息,使用这些信息进行鉴权和授权,并响应客户端请求。
优点
- 可扩展性:
JWT可以包含任意数量的声明,可以根据需要灵活地扩展和定制。 - 简单性:
JWT的结构简单明了,易于使用和理解。 - 安全性:
JWT使用签名保证了数据的完整性和可靠性,防止数据被篡改和伪造。 - 无状态性:
JWT不需要在服务器端存储状态信息,所有信息都被包含在JWT中,降低了服务器的负担。
代码示例
Go 后端
package main
import (
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"time"
)
const (
// JWT 密钥
SECRET_KEY = "random_strings"
)
type PayloadClaims struct {
Name string `json:"name"`
Email string `json:"email"`
jwt.StandardClaims
}
func (c PayloadClaims) Valid() error {
if err := c.StandardClaims.Valid(); err != nil {
return err
}
if c.Name == "" {
return errors.New("missing account name")
}
if c.Email == "" {
return errors.New("missing email")
}
return nil
}
// 构建JWT
func GenerateJWT(payload PayloadClaims) (string, error) {
// 生成 JWT Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
// 获取 JWT 密钥
s := []byte(SECRET_KEY)
// 对 JWT 进行签名
tokenString, err := token.SignedString(s)
if err != nil {
return "", err
}
return tokenString, nil
}
// 验证JWT
func ValidateJWT(token string) (*PayloadClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &PayloadClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(SECRET_KEY), nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*PayloadClaims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
func main() {
// 计算两小时后的时间戳
expiresAt := time.Now().Add(2 * time.Hour).Unix()
// 构造用户数据
profile := PayloadClaims{
// 自定义声明
Name: "Mystic", // Name 字段
Email: "mystic@example.com", // Email 字段
// 标准声明
StandardClaims: jwt.StandardClaims{
Audience: "Mystic", // JWT 的接收者,用于标识 JWT 的预期接收者
ExpiresAt: expiresAt, // JWT 的过期时间,用 Unix 时间戳表示(单位:秒),在过期时间之后,接收方不应再接受这个 JWT
Issuer: "admin", // JWT 的发行者,用于标识 JWT 的颁发者
Subject: "example", // JWT 的主题,用于标识 JWT 的主题或拥有者
},
}
// 获取 JWT 字符串
token, _ := GenerateJWT(profile)
fmt.Println(token)
// 解析 JWT 载荷信息
if claims, err := ValidateJWT(token); err != nil {
panic(fmt.Sprintf("Invalid token:\n %v", err))
} else {
fmt.Println(claims.Name)
fmt.Println(claims.Email)
fmt.Println(claims.Audience)
fmt.Println(claims.ExpiresAt)
}
}
JS 前端
// 假设存储在浏览器 localStorage 中,先获取 jwt 字符串
let token = localStorage.getItem("jwtToken");
// 获取 jwt 的载荷部分
let payload = token.split(".")[1];
// 解析成 json 字符串
let jsonData = atob(payload);
// 反序列化
let objects = JSON.parse(jsonData);
// 输出信息
console.log(objects);