Si no está familiarizado con los JWT, es un excelente recurso para familiarizarse con ellos. Básicamente, un JWT es un token incluido en el
Authorization
El encabezado de una solicitud HTTP se puede usar para verificar al usuario que realiza la solicitud. Si bien la mayoría de las formas de autenticación de token requieren una lectura de la base de datos para verificar que el token pertenece a un usuario autenticado activo, al usar JWT, si el JWT se puede decodificar correctamente, eso garantiza que es un token válido, ya que tiene un campo de firma que se convertirá en no válido si algún dato en el token está dañado o manipulado. También puede decidir qué datos codificar en el cuerpo de JWT, lo que significa que al decodificar con éxito un JWT también puede obtener datos útiles, como el nombre de usuario o el correo electrónico de un usuario. El alcance de este artículo se limita a la creación de un middleware en Golang para verificar la validez de un JWT en una solicitud entrante. La generación de JWT es un proceso separado y no describiré cómo hacerlo aquí.Estoy usando una aplicación web simple de Golang que proporciona un punto final de API único:
/ping
, que responde con "pong". No se han utilizado bibliotecas en el siguiente código. package main import ( "log" "net/http" ) func pong (w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([] byte ( "pong" )) } func main () { http.Handle( "/ping" , http.HandlerFunc(pong)) log.Fatal(http.ListenAndServe( ":8080" , nil )) }
La función principal registra
pong
como la función de controlador para el /ping
punto final Luego inicia un servidor HTTP que se ejecuta en el puerto 8080. La función pong
simplemente responde con un estado de 200 (que significa "OK") y el texto "pong". Handle
requiere que su segundo argumento sea un valor que implemente el Handler
interfaz, por lo que HandlerFunc
es una función de adaptador que hace precisamente eso. Más sobre esto más adelante.Así que ahora si ejecutas esto con
go run main.go
(Suponiendo que el nombre del archivo es main.go
) y envíe una solicitud GET a //localhost:8080/ping
(Abrir este enlace en su navegador es una manera fácil) obtendrá el texto pong
.Ahora quiero proteger el
/ping
punto final para que solo las solicitudes entrantes que tengan un JWT válido puedan obtener la respuesta requerida. Si el JWT no está presente o está dañado, la aplicación debe devolver el código de estado HTTP 401: no autorizado.Un middleware para nuestro servidor HTTP debería ser una función que admita una función que implemente el
http.Handler
interfaz y devuelve una nueva función que implementa la http.Handler
interfaz. Este concepto debería ser familiar para cualquier persona que haya usado cierres de JavaScript, decoradores de Python o programación funcional de algún tipo. Pero esencialmente, es una función que toma una función y le agrega funcionalidad adicional.Pero espera, la función de controlador que acabamos de escribir,
pong
, no implementa esta interfaz. Así que tenemos que cuidar eso. Si miras en el integrado de Golang http
paquete encontrará que el Handler
interfaz solo especifica que el ServeHTTP
debe implementarse el método para ese valor. De hecho, ni siquiera tenemos que implementar esto nosotros mismos para nuestra función de controlador. http
también proporciona una práctica función auxiliar llamada HandlerFunc
que acepta cualquier función que acepte ResponseWriter
y *Request
como parámetros, y lo convierte en una función que implementa el Handler
interfaz.En este punto es fácil confundirse entre estas 4 cosas en el
http
paquete, así que permítanme aclararlos: http.Handler
- Una interfaz con un solo miembro - ServeHTTP
. http.HandlerFunc
- Una función que se utiliza para convertir pong
(y cualquier otra función de controlador) a una función que implementa el Handler
interfaz. http.HandleFunc
- Una función utilizada para asociar un punto final con una función de controlador. Convierte automáticamente la función del controlador en una función que implementa la interfaz del controlador. No hemos usado esto aquí. http.Handle
- Igual que HandleFunc
, excepto que NO convierte la función del controlador en una función que implementa el Handler
interfaz. Vamos a crear la estructura de un middleware básico: func middleware (next http.Handler) http . Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { // Do stuff next.ServeHTTP(w, r) }) }
Como puede ver, recibimos la solicitud y podemos hacer lo que queramos con ella antes de llamar a la función del controlador que creamos anteriormente, que se pasa a nuestro middleware como
next
. hacemos uso de HandlerFunc
de nuevo para convertir la función volvemos a una función que implementa Handler
interfaz.Ahora puede hacer que su middleware haga lo que quiera con la solicitud entrante. Lo que quiero hacer es validar el JWT en el
Authorization
encabezado de la solicitud, así que comenzaré con los siguientes cambios: authHeader := strings.Split(r.Header.Get( "Authorization" ), "Bearer " ) if len (authHeader) != 2 { fmt.Println( "Malformed token" ) w.WriteHeader(http.StatusUnauthorized) w.Write([] byte ( "Malformed Token" )) }
Esto toma el token JWT del
Authorization
encabezamiento. Si el token no está presente, devuelve un estado no autorizado y nunca llama a nuestra función de controlador.El formato esperado del encabezado es
Bearer <token>
. Tenga en cuenta que puede pasar el JWT en la solicitud de la forma que desee, pero esta es la forma ampliamente aceptada de hacerlo.Si el token está realmente presente, tendremos que decodificarlo. Para esto estoy usando el
go-jwt
biblioteca, que se puede instalar con go get github.com/dgrijalva/jwt-go
else { jwtToken := authHeader[ 1 ] token, err := jwt.Parse(jwtToken, func (token *jwt.Token) ( interface {}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil , fmt.Errorf( "Unexpected signing method: %v" , token.Header[ "alg" ]) } return [] byte (SECRETKEY), nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { ctx := context.WithValue(r.Context(), "props" , claims) // Access context values in handlers like this // props, _ := r.Context().Value("props").(jwt.MapClaims) next.ServeHTTP(w, r.WithContext(ctx)) } else { fmt.Println(err) w.WriteHeader(http.StatusUnauthorized) w.Write([] byte ( "Unauthorized" )) } }
Usamos
jwt.Parse
para decodificar nuestro token. El segundo argumento de esta función es una función que se utiliza para devolver la clave secreta utilizada para decodificar el token después de verificar si el método de firma del token es HMAC. Esto se debe a que JWT se puede codificar de muchas maneras, incluido el cifrado asimétrico con un par de claves públicas y privadas. Aquí estoy usando una clave secreta simple para decodificar el JWT. Esta debe ser la misma clave secreta utilizada para codificar el JWT por la entidad que generó el JWT. El método de firma no será HMAC si la clave se codificó de alguna otra manera, por lo que verificamos esto primero.Tenga en cuenta que debe reemplazar el
SECRETKEY
variable con su clave secreta, que debe ser una cadena. Luego obtenemos los reclamos del token. Los reclamos son los valores que se han codificado en el JWT. Si la decodificación falla, significa que el token se ha dañado o manipulado y devolvemos un estado no autorizado.Si la decodificación fue exitosa, creamos una variable
ctx
para mantener estos reclamos y adjuntarlos a la instancia de solicitud a través de su Context
. Esto se hace para que podamos acceder a ellos en nuestra función de controlador (u otros middlewares si encadenamos varios middlewares) si es necesario, como se menciona en las líneas comentadas. "accesorios" aquí es una clave que utilicé, y se puede sustituir con cualquier valor de su elección siempre que use la misma clave mientras intenta obtener datos del contexto. Un caso de uso típico sería obtener el ID de usuario de las notificaciones en la función del controlador y usarlo para realizar una operación de base de datos para obtener información que corresponda a ese usuario. Finalmente, cambiamos nuestro código anterior para envolver la función del controlador en el middleware. http.Handle( "/ping" , middleware(http.HandlerFunc(pong)))
package main import ( "context" "fmt" "log" "net/http" "strings" "github.com/dgrijalva/jwt-go" ) func middleware (next http.Handler) http . Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { authHeader := strings.Split(r.Header.Get( "Authorization" ), "Bearer " ) if len (authHeader) != 2 { fmt.Println( "Malformed token" ) w.WriteHeader(http.StatusUnauthorized) w.Write([] byte ( "Malformed Token" )) } else { jwtToken := authHeader[ 1 ] token, err := jwt.Parse(jwtToken, func (token *jwt.Token) ( interface {}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil , fmt.Errorf( "Unexpected signing method: %v" , token.Header[ "alg" ]) } return [] byte (SECRETKEY), nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { ctx := context.WithValue(r.Context(), "props" , claims) // Access context values in handlers like this // props, _ := r.Context().Value("props").(jwt.MapClaims) next.ServeHTTP(w, r.WithContext(ctx)) } else { fmt.Println(err) w.WriteHeader(http.StatusUnauthorized) w.Write([] byte ( "Unauthorized" )) } } }) } func pong (w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([] byte ( "pong" )) } func main () { http.Handle( "/ping" , middleware(http.HandlerFunc(pong))) log.Fatal(http.ListenAndServe( ":8080" , nil )) }