User authentication mode Cookie-Session, JWT-Token (implemented by gin framework)

In computer networks, we know that HTTP is a stateless protocol. After a request is completed, the next time the server sends the request, it will not know who sent the request (the same IP does not represent the same user). In Web applications In , user authentication and authentication is a very important part. In practice, there are many available modes, and each has its own advantages.

Cookie-Session authentication mode

Introduction

In the early stage of web application development, most of them adopt the session management method based on Cookie-Session

  • The client uses username and password for authentication
  • The server verifies that the user name and password are correct, generates and stores the Session, and returns the SessionID to the client through the Cookie
  • When the client accesses the interface that requires authentication, the SessionID is carried in the cookie
  • The server searches for the Session through the SessionID and performs authentication, and returns the required data to the client
    insert image description here

code example

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!")
}

Advantages and disadvantages

advantage:

  • The session-cookie authentication mechanism can be supported on basically all web browsers
  • Easy to implement

shortcoming:

  • The server needs to store the Session, and because the Session needs to be frequently and quickly searched, it is usually stored in memory or in a memory database. If there are a large number of online users, it will take up a lot of server resources.
  • When expansion is required, the server that creates the Session may not be the server that authenticates the Session, so all Sessions need to be stored and shared separately.
  • Since the client uses cookies to store the SessionID, compatibility processing is required in cross-domain scenarios, and it is also difficult to prevent CSRF attacks in this way.

Token authentication mode

Introduction

In view of the above-mentioned shortcomings of the Session-based session management method, the Token-based stateless session method (the server does not store information) was born.
The so-called Token is actually a string of encrypted strings generated by the server as a "token" for the client to request. When the user successfully logs in with the account password for the first time, the server generates a Token and the expiration time of the Token and returns this to the client. If the login is successful, the client only needs to bring the Token within the valid time to request The data is enough, no need to bring the username and password again.

The logic is as follows:

  • The client uses username and password for authentication

  • After the server verifies that the user name and password are correct, it generates a Token and returns it to the client

  • The client saves the Token, and accessing the interface that requires authentication is to add the Token to the URL parameter or HTTP Header

  • The server authenticates by decoding the Token, and returns the required data to the client.
    insert image description here
    The Token-based session management method effectively solves the problems caused by the Session-based session management method.

  • The server does not need to store information related to user authentication, the authentication information will be encrypted into the Token, and the server only needs to read the authentication information contained in the Token

  • Avoid the problem of difficult expansion caused by shared Session

  • No need to rely on cookies, effectively avoiding CSRF attacks caused by cookies

  • Using CORS can quickly solve cross-domain problems

Introduction to JWT

JSON Web Token (JWT) is a JSON-based open standard ( RFC 7519 ) implemented for passing claims between web application environments, which defines a compact and independent way to communicate between parties Securely transmit information in the form of JSON objects. This information can be verified and trusted because it is digitally signed. JWT can be signed with a private key (using the HMAC algorithm) or with a public/private key pair using RSA or ECDSA.

JWT itself is not set for any technical implementation, it just defines a Token-based session management rule, covering the standard content that Token needs to contain and the Token generation process, especially suitable for single sign-on (SSO) scenarios of distributed sites .

Here are some scenarios where JSON Web Tokens are used:

  • Authorization: This is the most common scenario for using JWT. Once the user is logged in, every subsequent request will contain the JWT, allowing the user to access the routes, services, and resources allowed by that token. Single sign-on is a feature where JWT is widely used today because of its low overhead and its ability to be easily used across different domains.
  • Information Exchange: JSON Web Tokens are a great way to securely transfer information between parties. Because JWTs can be signed (for example, using a public/private key pair), you can be sure that senders are who they say they are. Also, since the signature is calculated using the header and payload, you can also verify that the content has not been tampered with.

JWT structure

.A JWT token consists of three parts separated by dots :

  • Header
  • Payload
  • Signature (Signature

A JWT typically looks like this:

xxxxx.yyyyy.zzzzz

insert image description here

The header and payload exist in the form of JSON, which is the JSON in JWT. The contents of the three parts are separately encoded by Base64 to be spliced .​​into a JWT Token

Header

The header (Header) usually consists of two parts: the type of token (JWT) and the signature algorithm used (such as HMAC, SHA256 or RSA).

For example:

{
    
    
  "alg": "HS256",
  "typ": "JWT"
}

Base64Url encoding this JSON forms the first part of the JWT.

Payload

The second part of the token is the payload, which contains the claims. Claims are statements about entities (usually users) and attached data.
There are three types of declarations: registered declarations, public declarations, and private declarations.

  • Registered Declarations : These are a set of predefined declarations that are not mandatory but recommended to provide a useful, interoperable set of declarations.

iss (issuer): issuer/issuer
exp (expiration time): expiration time
sub (subject): subject
aud (audience): audience
nbf (Not Before): effective time
iat (Issued At): issue time
jti (JWT ID ):serial number

Claim names are only three characters long because JWTs are designed to be compact.

  • Public claims : These can be freely defined by the person using the JWT. But to avoid collisions, they should be defined in the IANA JSON Web Token Registry , or as URIs that include collision-resistant namespaces.
  • Private Statements : Custom statements created to share information between parties who agree to use them, and are neither registration statements nor public statements.

Load instance:

{
    
    
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

The payload is Base64Url encoded to form the second part of the JSON Web Token.

注意,对于签名令牌,此信息虽然受到防止篡改的保护,但任何人都可以读取。除非加密,否则请勿将秘密信息放入 JWT 的有效负载或标头元素中。

Signature

To create the signed part, you have to take the encoded header, the encoded payload, the key, the algorithm specified in the header, and sign it.

For example, if you want to use the HMAC SHA256 algorithm, the signature will be created by:

You need to specify a key (secret). This key is known only to the server and cannot be disclosed to the user. Then, use the signature algorithm specified in the Header (the default is HMAC SHA256) to generate a signature according to the following formula.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

code example

// 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")
}

Advantages and disadvantages of JWT

JWT has all the advantages of the Token-based session management method, and does not rely on cookies, so that it can prevent CSRF attacks and can also run normally in the browser environment where cookies are disabled.

The biggest advantage of JWT is that the server no longer needs to store the Session, so that the server-side authentication and authentication business can be easily expanded without being stored in the Token. Once the JWT Token is issued, it will always be available within the validity period and cannot be abolished on the server. The user can only log out by relying on the client to delete the JWT Token stored locally. If the user needs to be disabled, JWT cannot be used alone.

Access Token and Refresh Token authentication mode

The Tokens mentioned above are all Access Tokens, that is, the Tokens required to access resource interfaces. There is another kind of Token, Refresh Token. Usually, the validity period of Refresh Token will be longer, while the validity period of Access Token is relatively long. Short, when the Access Token expires due to expiration, a new Access Token can be obtained by using the Refresh Token. If the Refresh Token also expires, the user can only log in again.

  • Client uses username and password for authentication
  • The server generates an Access Token with a shorter validity period (for example, 10 minutes), and a Refresh Token with a longer validity period (for example, 7 days)
  • When the client accesses the interface that requires authentication, it carries the Access Token
  • If the Access Token has not expired, the server will return the required data to the client after authentication
  • If the authentication fails (for example, a 401 error is returned) when accessing an interface that requires authentication with an Access Token, the client uses Refresh Token that has not expired, and the server sends a new Access Token to the client
  • If the Refresh Token has not expired, the server sends a new Access Token to the client
  • The client uses the new Access Token to access the interface that requires authentication
    insert image description here

code example


//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")
}

Guess you like

Origin blog.csdn.net/m0_53328239/article/details/131724576