Golang 实现JWT认证

Golang 实现JWT认证

认证是让应用知道给应用发送请求的人是他所说的那个人。JSON web token (JWT)是认证的一种方式,相比于基于Session认证,在系统中并不存储任何关于用户信息。

本文演示使用Golang实现基于JWT认证的示例应用。

1. JWT

1.1. JWT格式

假设用户user1尝试登录应用,成功后收到token信息如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54

这就是JWT,有三个部分组成,使用.分隔。

1). 第一部分是头部(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9)。头部信息是生成签名的算法,这部分非常标准,对于任何使用相同算法的JWT都是一样的。
2). 第二部分是有效载荷(eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ),它包含特定应用程序信息(在我们的示例中是用户名),以及关于token有效期和有效性的信息。
3). 第三部分是签名(2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54)。它是通过将前两部分与一个密钥合并和散列而生成的。

有趣的是头部和有效载荷是不加密的。仅使用base64进行编码,意味着任何人都能解码看到内容。使用下面命令(linux)可以看到:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d

返回:

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

类似方式可以看到第二部分内容:

{ "username": "user1", "exp": 1547974082 }

1.2. JWT签名

既然JWT前两部分任何人都可以读写,JWT安全由什么来保证呢?答案是最后部分(签名)是如何生成的。

假设应用程序希望向成功登录的用户(例如user1)发出JWT。
头部和有效负载非常简单:报头或多或少是固定的,而有效负载JSON对象是通过设置用户ID和终止时间(以unix毫秒为单位)形成的。

发出token的应用程序也有一个密钥,只有应用程序本身知道。应用程序将报头和有效负载的base64表示形式与密钥组合,然后通过一个散列算法(在本例中是HS256,报头中指定的)进行传递。
Golang 实现JWT认证_第1张图片

算法是如何实现的细节超出了本文的范围,但要注意的是,这是一个方式,这意味着我们不能反向算法和获得的签名并使用…所以我们的密钥需保护好。

1.3. 验证JWT

为了验证请求的JWT,再次使用请求的报头和有效载荷以及密钥生成签名。如果签名匹配那么JWT为有效。

假设一个试图发出假token黑客,他可以轻松地生成报头和有效负载,但是如果不知道密钥,就无法生成有效的签名。如果试图篡改有效JWT的现有有效负载,签名将不再匹配。
Golang 实现JWT认证_第2张图片

通过这种方式JWT作为一种安全方式认证用户,在应用服务器上无需存储任何信息(除了密钥)。

2. Golang实现示例

现在我们已经了解JWT的认证机制,下面在GoLang中实现。

2.1. 创建HTTP服务器

首先初始化HTTP服务及必要的路由:

package main

import (
	"log"
	"net/http"
)

func main() {
	// "Signin" and "Welcome" are the handlers that we will implement
	http.HandleFunc("/signin", Signin)
	http.HandleFunc("/welcome", Welcome)
	http.HandleFunc("/refresh", Refresh)

	// start the server on port 8000
	log.Fatal(http.ListenAndServe(":8000", nil))
}

下面实现Signin和Welcome两个路由。

2.2. 处理用户登录

/signin路由使用用户凭证登录,为了简化我们在内存中存储用户信息:

// Create the JWT key used to create the signature
var jwtKey = []byte("my_secret_key")

var users = map[string]string{
	"user1": "password1",
	"user2": "password2",
}

// Create a struct to read the username and password from the request body
type Credentials struct {
	Password string `json:"password"`
	Username string `json:"username"`
}

// Create a struct that will be encoded to a JWT.
// We add jwt.StandardClaims as an embedded type, to provide fields like expiry time
type Claims struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

// Create the Signin handler
func Signin(w http.ResponseWriter, r *http.Request) {
	var creds Credentials
	// Get the JSON body and decode into credentials
	err := json.NewDecoder(r.Body).Decode(&creds)
	if err != nil {
		// If the structure of the body is wrong, return an HTTP error
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Get the expected password from our in memory map
	expectedPassword, ok := users[creds.Username]

	// If a password exists for the given user
	// AND, if it is the same as the password we received, the we can move ahead
	// if NOT, then we return an "Unauthorized" status
	if !ok || expectedPassword != creds.Password {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// Declare the expiration time of the token
	// here, we have kept it as 5 minutes
	expirationTime := time.Now().Add(5 * time.Minute)
	// Create the JWT claims, which includes the username and expiry time
	claims := &Claims{
		Username: creds.Username,
		StandardClaims: jwt.StandardClaims{
			// In JWT, the expiry time is expressed as unix milliseconds
			ExpiresAt: expirationTime.Unix(),
		},
	}

	// Declare the token with the algorithm used for signing, and the claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// Create the JWT string
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		// If there is an error in creating the JWT return an internal server error
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// Finally, we set the client cookie for "token" as the JWT we just generated
	// we also set an expiry time which is the same as the token itself
	http.SetCookie(w, &http.Cookie{
		Name:    "token",
		Value:   tokenString,
		Expires: expirationTime,
	})
}

如果用户登录凭证正确,该处理器在客户通过cookie设置JWT。客户端有了cookie,后续每个请求将携带cookie。下面我们实现Welcome处理器。

2.3. 处理后续认证

登录成功用户在客户端存储了会话信息,可以使用会话信息进行:

  • 认证后续用户请求
  • 获取用户请求的信息

Welcome处理器实现:

func Welcome(w http.ResponseWriter, r *http.Request) {
	// We can obtain the session token from the requests cookies, which come with every request
	c, err := r.Cookie("token")
	if err != nil {
		if err == http.ErrNoCookie {
			// If the cookie is not set, return an unauthorized status
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		// For any other type of error, return a bad request status
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Get the JWT string from the cookie
	tknStr := c.Value

	// Initialize a new instance of `Claims`
	claims := &Claims{}

	// Parse the JWT string and store the result in `claims`.
	// Note that we are passing the key in this method as well. This method will return an error
	// if the token is invalid (if it has expired according to the expiry time we set on sign in),
	// or if the signature does not match
	tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})
	if err != nil {
		if err == jwt.ErrSignatureInvalid {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if !tkn.Valid {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// Finally, return the welcome message to the user, along with their
	// username given in the token
	w.Write([]byte(fmt.Sprintf("Welcome %s!", claims.Username)))
}

2.4. 更新token

该示例中,有效时间为5分钟。用户当然不希望每5分钟登录一次。因此我们实现另一个路由/refresh,使用之前的token(仍然有效)获取带有更新时间的token。

为了尽量少使用JWT,过期时间通常保持在几分钟左右。通常,客户机应用程序将在后台刷新令牌。


func Refresh(w http.ResponseWriter, r *http.Request) {
	// (BEGIN) The code uptil this point is the same as the first part of the `Welcome` route
	c, err := r.Cookie("token")
	if err != nil {
		if err == http.ErrNoCookie {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	tknStr := c.Value
	claims := &Claims{}
	tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})
	if err != nil {
		if err == jwt.ErrSignatureInvalid {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if !tkn.Valid {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	// (END) The code up-till this point is the same as the first part of the `Welcome` route

	// We ensure that a new token is not issued until enough time has elapsed
	// In this case, a new token will only be issued if the old token is within
	// 30 seconds of expiry. Otherwise, return a bad request status
	if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Now, create a new token for the current use, with a renewed expiration time
	expirationTime := time.Now().Add(5 * time.Minute)
	claims.ExpiresAt = expirationTime.Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// Set the new token as the users `token` cookie
	http.SetCookie(w, &http.Cookie{
		Name:    "token",
		Value:   tokenString,
		Expires: expirationTime,
	})
}

3. 运行应用

编译并运行程序:

go build
./jwtDemo

现在使用支持cookie的HTTP客户端工具进行测试,如POSTMAN,使用凭证进行登录:

POST http://localhost:8000/signin

{"username":"user1","password":"password1"}

下面可以使用同样的工具发送welcome请求获取返回信息:

GET http://localhost:8000/welcome

发送更新token请求,然后检查客户端cookie及token新的值:

POST http://localhost:8000/refresh

4. 总结

本文介绍了JWT认证机制,通过示例使用Golang进行演示。

你可能感兴趣的:(Golang)