在计算机网络中,我们知道HTTP是一个无状态的协议,一次请求结束后,下次再发送服务器就不知道这个请求是谁发来的了(同一个IP不代表同一个用户),在Web应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用模式,并且各有千秋。
在Web应用发展的初期,大部分采用基于Cookie-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的会话管理方式存在上述的多个缺点,基于Token的无状态(服务端不存储信息)会话方式诞生了。
所谓的Token,其实就是服务端生成的一串加密字符串、以作客户端进行请求的一个“令牌”。当用户第一次使用账号密码成功进行登录后,服务器便生成一个Token及Token失效时间并将此返回给客户端,若成功登陆,以后客户端只需在有效时间内带上这个Token前来请求数据即可,无需再次带上用户名和密码。
逻辑如下:
客户端使用用户名、密码进行认证
服务端验证用户名、密码正确后生成Token返回给客户端
客户端保存Token,访问需要认证的接口是在URL参数或HTTP Header中加入Token
服务端通过解码Token进行鉴权,返回给客户端需要的数据
基于Token的会话管理方式有效解决了基于Session的会话管理方式带来的问题。
服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到Token中,服务端只需要读取Token中包含的鉴权信息即可
避免了共享Session导致的不易扩展问题
不需要依赖Cookie,有效避免Cookie带来的CSRF攻击问题
使用CORS可以快速解决跨域问题
JSON Web Token (JWT) 是一种为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准 ( RFC 7519 ),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JWT本身没有定于任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO)场景。
以下是 JSON Web 令牌使用的一些场景:
JWT令牌是由点.
分隔的三个部分组成:
JWT通常如下所示:
xxxxx.yyyyy.zzzzz
头部和负载以JSON的形式存在,这就是JWT中的JSON,三部分的内容都分别单独经过了Base64编码,以.
拼接成一个JWT Token
标头(Header)通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(例如HMAC、SHA256或RSA)。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
对该JSON进行Base64Url编码形成JWT的第一部分。
令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。
声明分为三种类型:注册声明、公开声明和私人声明。
iss(issuer):签发人/发行者
exp(expiration time):过期时间
sub(subject):主题
aud(audience):受众
nbf(Not Before):生效时间
iat(Issued At):签发时间
jti(JWT ID):编号
声明名称只有三个字符长,因为JWT旨在紧凑。
负载实例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。
注意,对于签名令牌,此信息虽然受到防止篡改的保护,但任何人都可以读取。除非加密,否则请勿将秘密信息放入 JWT 的有效负载或标头元素中。
要创建签名部分,您必须获取编码的标头、编码的有效负载、密钥、标头中指定的算法,然后对其进行签名。
例如,如果要使用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拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务器不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储在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也失效了,用户就只能重新登录了。
//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")
}