Golang中使用 JWT认证来 保障Restful JSON API的安全(英文)

来源:互联网 时间:1970-01-01

In this post, we will not only cover how to use Go to create a RESTful JSON API, but we will also describe how protect our API with JSON Web Tokens (JWT).

What is JSON Web Token (JWT)?

Accoding to the official site ( http://jwt.io/ ):

JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).

A JWT consists of three main components: a header object, a claims object, and a signature. These three properties are encoded using base64, then concatenated with periods as separators.

The JWT claims object contains security information about the message. For example:

iss. Is the issuer of the claim. Connect uses it to identify the application making the call.

iat. Issued-at time. Contains the UTC Unix time at which this token was issued. There are no hard requirements around this claim but it does not make sense for it to be significantly in the future. Also, significantly old issued-at times may indicate the replay of suspiciously old tokens.

sub. The subject of this token. This is the user associated with the relevant action, and may not be present if there is no logged in user.

Authentication with JWT

JSON Web Tokens (JWT) are a more modern approach to authentication. As the web moves to a greater separation between the client and server, JWT provides a wonderful alternative to traditional cookie based authentication models.

JWTs provide a way for clients to authenticate every request without having to maintain a session or repeatedly pass login credentials to the server.

Benefits of using a token-based approach

Cross-domain / CORS: cookies + CORS don't play well across different domains. A token-based approach allows you to make AJAX calls to any server, on any domain because you use an HTTP header to transmit the user information.

Stateless: there is no need to keep a session store, the token is a self-contanined entity that conveys all the user information.

CDN: you can serve all the assets of your app from a CDN (e.g. javascript, HTML, images, etc.), and your server side is just the API.

Decoupling: you are not tied to a particular authentication scheme. The token might be generated anywhere, hence your API can be called from anywhere with a single way of authenticating those calls.

Mobile ready: when you start working on a native platform (iOS, Android, Windows 8, etc.) cookies are not ideal when consuming a secure API (you have to deal with cookie containers). Adopting a token-based approach simplifies this a lot.

CRSF: since you are not relying on cookies, you don't need to protect against cross site requests.

Organizing our Application code

Why modular? Having all of our functionality in different modules helps us in many ways:

The overall application layout is easier to understand. You can see how the parts work together since modules have to be injected before use. Code is reusable since all of the necessary functionality is contained inside the module. Testing your code is much easier.

Here is how we want our file structure to look like:

Creating Routers

The code shown below creates a router assigning each route with his controller who runs when that endpoint is called.


package routersimport ( "github.com/gorilla/mux")func InitRoutes() *mux.Router { router := mux.NewRouter()router = SetHelloRoutes(router)router = SetAuthenticationRoutes(router)return router}


package routersimport ( "api.jwt.auth/controllers""api.jwt.auth/core/authentication""github.com/codegangsta/negroni""github.com/gorilla/mux")func SetAuthenticationRoutes(router *mux.Router) *mux.Router { router.HandleFunc("/token-auth", controllers.Login).Methods("POST")router.Handle("/refresh-token-auth",negroni.New(negroni.HandlerFunc(authentication.RequireTokenAuthentication),negroni.HandlerFunc(controllers.RefreshToken),)).Methods("GET")router.Handle("/logout",negroni.New(negroni.HandlerFunc(authentication.RequireTokenAuthentication),negroni.HandlerFunc(controllers.Logout),)).Methods("GET")return router}


package routersimport ( "api.jwt.auth/controllers""github.com/codegangsta/negroni""github.com/gorilla/mux")func SetHelloRoutes(router *mux.Router) *mux.Router { router.Handle("/test/hello",negroni.New(negroni.HandlerFunc(controllers.HelloController),)).Methods("GET")return router}

Now, you will also notice now that the endpoint "/test/hello" has no "RequireTokenAuthentication" middleware (at least for now).

Creating Controllers

Let's create our controller's package to manage requests and responses. Controllers will interact with our services.


package controllersimport ( "api.jwt.auth/services""api.jwt.auth/services/models""encoding/json""net/http")func Login(w http.ResponseWriter, r *http.Request) { requestUser := new(models.User)decoder := json.NewDecoder(r.Body)decoder.Decode(&requestUser)responseStatus, token := services.Login(requestUser)w.Header().Set("Content-Type", "application/json")w.WriteHeader(responseStatus)w.Write(token)}func RefreshToken(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { requestUser := new(models.User)decoder := json.NewDecoder(r.Body)decoder.Decode(&requestUser)w.Header().Set("Content-Type", "application/json")w.Write(services.RefreshToken(requestUser))}func Logout(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { err := services.Logout(r)w.Header().Set("Content-Type", "application/json")if err != nil {w.WriteHeader(http.StatusInternalServerError)} else {w.WriteHeader(http.StatusOK)}}


package controllersimport ( "net/http")func HelloController(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { w.Write([]byte("Hello, World!"))} Creating models

Now that we have routes and controllers in place, it's time to create a basic user model that we can use to authenticate requests.


package modelstype User struct { UUID string `json:"uuid" form:"-"` Username string `json:"username" form:"username"` Password string `json:"password" form:"password"`} Creating Services


package servicesimport ( "api.jwt.auth/api/parameters""api.jwt.auth/core/authentication""api.jwt.auth/services/models""encoding/json"jwt "github.com/dgrijalva/jwt-go""net/http")func Login(requestUser *models.User) (int, []byte) { authBackend := authentication.InitJWTAuthenticationBackend()if authBackend.Authenticate(requestUser) {token, err := authBackend.GenerateToken(requestUser.UUID)if err != nil {return http.StatusInternalServerError, []byte("")} else {response, _ := json.Marshal(parameters.TokenAuthentication{token})return http.StatusOK, response}}return http.StatusUnauthorized, []byte("")}func RefreshToken(requestUser *models.User) []byte { authBackend := authentication.InitJWTAuthenticationBackend()token, err := authBackend.GenerateToken(requestUser.UUID)if err != nil {panic(err)}response, err := json.Marshal(parameters.TokenAuthentication{token})if err != nil {panic(err)}return response}func Logout(req *http.Request) error { authBackend := authentication.InitJWTAuthenticationBackend()tokenRequest, err := jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) {return authBackend.PublicKey, nil})if err != nil {return err}tokenString := req.Header.Get("Authorization")return authBackend.Logout(tokenString, tokenRequest)} Web server


package mainimport ( "api.jwt.auth/routers""api.jwt.auth/settings""github.com/codegangsta/negroni""net/http")func main() { settings.Init()router := routers.InitRoutes()n := negroni.Classic()n.UseHandler(router)http.ListenAndServe(":5000", n)} Let's try our luck!

If we try to test it, we will obtain a valid response from server:

At this moment we have not needed to send a valid token to the server to obtain a valid response

Applying authentication with JWT

As we can see right now, our API endpoint "/test/hello" is insecure. Let's add our "RequireTokenAuthentication" middleware to protect it.


package authenticationimport ( jwt "github.com/dgrijalva/jwt-go""net/http")func RequireTokenAuthentication(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) { authBackend := InitJWTAuthenticationBackend()token, err := jwt.ParseFromRequest(req,func(token *jwt.Token) (interface{}, error) {return authBackend.PublicKey, nil})if err == nil &&token.Valid &&!authBackend.IsInBlacklist(req.Header.Get("Authorization")) {next(rw, req)} else {rw.WriteHeader(http.StatusUnauthorized)}}


package routersimport ( "api.jwt.auth/controllers""api.jwt.auth/core/authentication""github.com/codegangsta/negroni""github.com/gorilla/mux")func SetHelloRoutes(router *mux.Router) *mux.Router { router.Handle("/test/hello",negroni.New(negroni.HandlerFunc(authentication.RequireTokenAuthentication),negroni.HandlerFunc(controllers.HelloController),)).Methods("GET")return router}

Now, our API test endpoint is secured and it's necessary a valid token to obtain a valid response.

Let's try our luck again!

If we can try again without a valid token, we'll receive a unauthorized error response:

...for the last time!

if we add an authorization header to our request we'll obtain a valid response from the server:

Firstly we need to obtain a valid token: Finally, we can do the request with our valid token: Session control & Token Logout

The last step, or bonus step, is how we can manage user logout and invalidate his token.

Imagine that an user token has been captured from third party or simply an user wants to logout from a client and we don't want his token to be valid again. In this way, we force he to do login again for security reasons.

To solve this problems, we'll use Redis to store all invalid tokens until their expiration.

For that, we need the methods: Logout and IsInBlacklist. The first method sets token's value in Redis with his expiration and the second method checks if a token is stored in redis


func (backend *JWTAuthenticationBackend) Logout(tokenString string, token *jwt.Token) error { redisConn := redis.Connect() expiration := backend.getTokenRemainingValidity(token.Claims["exp"]) return redisConn.SetValue(tokenString, tokenString, expiration)} func (backend *JWTAuthenticationBackend) IsInBlacklist(token string) bool { redisConn := redis.Connect()redisToken, _ := redisConn.GetValue(token)if redisToken == nil {return false}return true} Can I get all the code?

Yes! of course. Here is a repo with all of the code and tests.

https://github.com/brainattica/golang-jwt-authentication-api-sample Summary

That’s all you have to do. JWT is a fantastic and simple way to communicate trusted information across untrusted channels. Hope you find a good use for it soon!

As well as I hope you found this post useful and helped you.

Code less, compile quicker & execute faster => have more fun!