身份认证与授权方案

576 阅读6分钟

身份认证与授权方案

可以遗憾,但不要后悔。 

我们留在这里,从来不是身不由己。 

——— 而是选择在这里经历生活

概述

  • JWTToken + RedisSSO(OAuth2) 都是常见的身份验证和授权方案,它们都有自己的优缺点和适用场景,下面对它们进行比较。

  • 需要注意的是,这三种方案并不是完全互斥的,而是可以互相补充和组合。在实际应用中,应根据具体的场景和需求,选择最适合自己的方案。

常见方案对比

JWT

使用 JWT,可以在一定程度上替代 Redis 的某些使用场景。Redis 通常用于存储会话信息、缓存数据、消息队列等,而 JWT 可以用于传递会话信息、授权信息等,因此可以避免一些常规的 Redis 使用场景。

具体而言,使用 JWT 可以实现无状态(stateless)的身份验证和授权,不需要在服务器端存储会话信息,也不需要每次请求都从 Redis 中获取数据。这样可以减轻服务器的负担,提高系统的性能和可扩展性。同时,JWT 还可以通过设置过期时间和刷新令牌等方式,实现会话管理和安全性控制,进一步简化了服务器端的逻辑。

用户基本信息可以直接放在 JWTpayload 中,注意敏感信息不要放在这里面。如 usernamenicknameroleemailempid 等。

对比说明
优点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 协议的,因此也需要考虑一些常见的安全风险,例如 XSSCSRF 等。

组成部分

JWT 由三部分组成:头部 (header)、载荷 (payload) 和签名 (signature)

  • 头部:描述了生成的 JWT 的元数据,通常包括两个属性:算法 (alg) 和类型 (typ),这里使用的是 JSON 格式。
  • 载荷:包含了需要传递的信息(称为声明),可以包含多个声明,通常包括用户身份信息、访问权限、过期时间等,这里使用的是 JSON 格式。
  • 签名:用于验证 JWT 的完整性和可靠性,通常使用一个密钥 (secret) 进行签名,并将头部和载荷的内容与密钥一起进行哈希计算,以生成签名。

验证流程

JWT 的生成和验证流程如下:

  1. 服务器生成 JWT,使用算法和密钥对头部和载荷进行签名,将签名和 JWT 发送给客户端。
  2. 客户端将 JWT 存储在本地,并在请求服务器时将其作为 Authorization 头部或 Cookie 发送给服务器。
  3. 服务器接收到请求,从 Authorization 头部或 Cookie 中获取 JWT,并对其进行解密,验证签名和过期时间。
  4. 如果 JWT 验证通过,则从载荷中获取用户身份信息,使用这些信息进行鉴权和授权,并响应客户端请求。

优点

  1. 可扩展性:JWT 可以包含任意数量的声明,可以根据需要灵活地扩展和定制。
  2. 简单性:JWT 的结构简单明了,易于使用和理解。
  3. 安全性:JWT 使用签名保证了数据的完整性和可靠性,防止数据被篡改和伪造。
  4. 无状态性: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);