用户认证
- Cookie-Session认证模式
- 简介
- 代码示例
- 优缺点
- Token认证模式
- 简介
- JWT介绍
- JWT结构
- 标头(Header)
- 负载(Payload)
- 签名(Signature)
- 代码示例
- JWT优缺点
- Access Token和Refresh Token认证模式
- 代码示例
在计算机网络中,我们知道HTTP是一个无状态的协议,一次请求结束后,下次再发送服务器就不知道这个请求是谁发来的了(同一个IP不代表同一个用户),在Web应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用模式,并且各有千秋。
Cookie-Session认证模式
简介
在Web应用发展的初期,大部分采用基于Cookie-Session的会话管理方式
- 客户端使用用户名、密码进行认证
- 服务端验证用户名、密码正确后生成并存储Session,将SessionID通过Cookie返回给客户端
- 客户端访问需要认证的接口时在Cookie中携带SessionID
- 服务端通过SessionID查找Session并进行鉴权,返回给客户端需要的数据
代码示例
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"log"
"net/http"
"time"
)
var Rdb *redis.Client //redis全局变量
type UserLogin struct { //登录入参
UserName string `json:"user_name"`
Password string `json:"password"`
}
func redisStart() {
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "123456",
DB: 0,
PoolSize: 100,
})
_, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
ctx := context.Background()
pong, err := rdb.Ping(ctx).Result()
fmt.Println(pong)
if err != nil {
log.Println(err)
}
Rdb = rdb
}
func main() {
redisStart()
router := gin.Default()
// 设置登录请求的路由处理函数
router.POST("/login", loginHandler)
// 设置受保护页面的路由处理函数
router.GET("/protected", protectedHandler)
router.Run(":8080")
}
func loginHandler(c *gin.Context) {
var user UserLogin
if err := c.ShouldBindJSON(&user); err != nil {
log.Println(err)
return
}
// 模拟检查用户名和密码是否匹配
// 这里应该是与数据库中的用户名和密码进行比对
if user.UserName == "用户的用户名" && user.Password == "用户的密码" {
//生成一个Session ID
sessionID := "你自己设置的sessionID"
// 将Session ID 作为键 存储到redis中,并设置过期时间(此处为30分钟)
Rdb.Set(context.Background(), sessionID, "你想存储的用户信息", time.Minute*30)
//创建Cookie ,将Session ID 设置为Cookie的值
/*
name:"session"
value:sessionID
失效时间:3600
path:指定cookie在哪个路径(路由)下生效,默认是`\`
domain:指定cookie所属域名,默认是当前域名
secure:该cookie是否被使用安全协议传输。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false
httpOnly:如果给某个cookie设置了httpOnly属性,则无法通过js脚本读取到该cookie的信息。
*/
c.SetCookie("session", sessionID, 3600, "/", "localhost", false, true)
c.Redirect(http.StatusFound, "/protected")
} else {
// 登录失败
c.String(http.StatusUnauthorized, "Invalid username or password")
}
}
func protectedHandler(c *gin.Context) {
//检查是否存在session cookie
cookie, err := c.Cookie("session")
if err != nil || cookie == "" {
c.Redirect(http.StatusOK, "/login")
return
}
//检查session是否存在且过期
isOk := Rdb.Exists(context.Background(), cookie).Val()
duration := Rdb.TTL(context.Background(), cookie).Val()
if isOk == 0 || duration <= 0 {
c.Redirect(http.StatusFound, "/login")
return
}
// 受保护页面的逻辑
c.String(http.StatusOK, "Welcome to the protected page!")
}
优缺点
优点:
- session-cookie 认证机制在基本上所有的网页浏览器上都能够支持
- 实现方式简单
缺点:
- 服务端需要存储Session,并且由于Session需要经常快速查找,通常存储在内存或内存数据库中 ,如果在线用户的人数较多时,会占用大量的服务器资源。
- 当需要扩展时,创建Session的服务器可能不是验证Session的服务器,所以还需要将所有Session单独存储并共享。
- 由于客户端使用Cookie存储SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范CSRF攻击。
Token认证模式
简介
鉴于基于Session的会话管理方式存在上述的多个缺点,基于Token的无状态(服务端不存储信息)会话方式诞生了。
所谓的Token,其实就是服务端生成的一串加密字符串、以作客户端进行请求的一个“令牌”。当用户第一次使用账号密码成功进行登录后,服务器便生成一个Token及Token失效时间并将此返回给客户端,若成功登陆,以后客户端只需在有效时间内带上这个Token前来请求数据即可,无需再次带上用户名和密码。
逻辑如下:
-
客户端使用用户名、密码进行认证
-
服务端验证用户名、密码正确后生成Token返回给客户端
-
客户端保存Token,访问需要认证的接口是在URL参数或HTTP Header中加入Token
-
服务端通过解码Token进行鉴权,返回给客户端需要的数据
基于Token的会话管理方式有效解决了基于Session的会话管理方式带来的问题。 -
服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到Token中,服务端只需要读取Token中包含的鉴权信息即可
-
避免了共享Session导致的不易扩展问题
-
不需要依赖Cookie,有效避免Cookie带来的CSRF攻击问题
-
使用CORS可以快速解决跨域问题
JWT介绍
JSON Web Token (JWT) 是一种为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准 ( RFC 7519 ),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JWT本身没有定于任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO)场景。
以下是 JSON Web 令牌使用的一些场景:
- 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够轻松地跨不同域使用。
- 信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以进行签名(例如,使用公钥/私钥对),所以您可以确定发送者就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
JWT结构
JWT令牌是由点.
分隔的三个部分组成:
- 标头(Header)
- 负载(Payload)
- 签名(Signature
JWT通常如下所示:
xxxxx.yyyyy.zzzzz
头部和负载以JSON的形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.
拼接成一个JWT Token
标头(Header)
标头(Header)通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(例如HMAC、SHA256或RSA)。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
对该JSON进行Base64Url编码形成JWT的第一部分。
负载(Payload)
令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。
声明分为三种类型:注册声明、公开声明和私人声明。
- 注册声明:这些是一组预定义的声明,不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。
iss(issuer):签发人/发行者
exp(expiration time):过期时间
sub(subject):主题
aud(audience):受众
nbf(Not Before):生效时间
iat(Issued At):签发时间
jti(JWT ID):编号
声明名称只有三个字符长,因为JWT旨在紧凑。
- 公共声明:这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在IANA JSON Web 令牌注册表中定义,或者定义为包含防冲突命名空间的 URI。
- 私人声明:为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。
负载实例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。
注意,对于签名令牌,此信息虽然受到防止篡改的保护,但任何人都可以读取。除非加密,否则请勿将秘密信息放入 JWT 的有效负载或标头元素中。
签名(Signature)
要创建签名部分,您必须获取编码的标头、编码的有效负载、密钥、标头中指定的算法,然后对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
你需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256)按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
代码示例
// JwtPayLoad jwt中payload数据
type JwtPayLoad struct {
Username string `json:"username"` //用户名
NickName string `json:"nick_name"` //昵称
Role int `json:"role"` // 权限 1 管理员 2 普通用户 3 游客
UserID uint `json:"user_id"` //用户id
}
type CustomClaims struct {
JwtPayLoad
jwt.StandardClaims
}
// GenToken 创建token
func GenToken(user JwtPayLoad) (string, error) {
var MySecret = []byte(global.Config.Jwy.Secret)
claim := CustomClaims{
user,
jwt.StandardClaims{
ExpiresAt: jwt.At(time.Now().Add(time.Hour * time.Duration(global.Config.Jwy.Expires))), //默认2小时过期
Issuer: global.Config.Jwy.Issuer, // 签发人
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
return token.SignedString(MySecret)
}
// ParseToken 解析token
func ParseToken(tokenStr string) (*CustomClaims, error) {
var MySecret = []byte(global.Config.Jwy.Secret)
token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return MySecret, nil
})
if err != nil {
global.Logger.Error(fmt.Sprintf("token parse errr: %s", err.Error()))
return nil, err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
JWT优缺点
JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务器不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储在Token中,JWT Token一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单独使用JWT就无法做到了。
Access Token和Refresh Token认证模式
前面提到的Token,都是Access Token,也就是访问资源接口时所需要的Token,还有另外一种Token,Refresh Token,通常情况下,Refresh Token的有效期会比较长,而Access Token的有效期比较短,当Access Token由于过期而失效时,使用Refresh Token就可以获取到新的Access Token,如果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访问需要认证的接口
代码示例
//AccessClaims
func (j *JWT) CreateAccessClaims(baseClaims request.BaseClaims) request.CustomClaims {
accessExpires, _ := time.ParseDuration(setting.Conf.JWT.AccessExpiresTime)
claims := request.CustomClaims{
TypeClaims: "accessClaims",
BaseClaims: baseClaims,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()), //签发时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessExpires)), // 过期时间 7天 配置文件
Issuer: setting.Conf.JWT.Issuer, // 签名的发行者
},
}
return claims
}
//refreshClaims
func (j *JWT) CreateRefreshClaims(baseClaims request.BaseClaims) request.CustomClaims {
RefreshExpires, _ := time.ParseDuration(setting.Conf.JWT.RefreshExpiresTime)
claims := request.CustomClaims{
TypeClaims: "refreshClaims",
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()), //签发时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshExpires)), // 过期时间 配置文件
Issuer: setting.Conf.JWT.Issuer, // 签名的发行者
},
}
return claims
}
// CreateToken 创建一个token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey) //SigningKey 秘钥 自己定义
}
// ParseToken 解析 token
func ParseToken(tokenStr string) (*CustomClaims, error) {
var MySecret = []byte(global.Config.Jwy.Secret)
token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return MySecret, nil
})
if err != nil {
global.Logger.Error(fmt.Sprintf("token parse errr: %s", err.Error()))
return nil, err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}