paint-brush
Por qué JWT es la clave para proteger su aplicación empresarial y por qué KumuluzEE podría ser su nuevo mejor amigopor@jesperancinha
Nueva Historia

Por qué JWT es la clave para proteger su aplicación empresarial y por qué KumuluzEE podría ser su nuevo mejor amigo

por João Esperancinha40m2025/01/17
Read on Terminal Reader

Demasiado Largo; Para Leer

"JWT" o JavaScript Object Notation Web Token es un estándar definido en [ RFC7519 ]. Puede definirse de múltiples maneras y puede usarse para transmitir información entre dos partes. En este artículo, veremos cómo integrar "JWT" en una aplicación empresarial Java común.
featured image - Por qué JWT es la clave para proteger su aplicación empresarial y por qué KumuluzEE podría ser su nuevo mejor amigo
João Esperancinha HackerNoon profile picture
0-item

Hoy en día, nos preocupamos cada vez más por el rendimiento y, al mismo tiempo, queremos saber cómo los sistemas pueden comunicarse de forma rápida y fiable. Muchas veces queremos enviar información y mantenerla lo más confidencial y segura posible. A veces, los datos confidenciales también deben moverse a través de la web de forma pública y desencadenar acciones en el otro extremo del cable. En la mayoría de los casos, queremos generar acciones que provoquen mutaciones de datos. En estos casos, no solo buscamos proteger nuestros datos. Queremos asegurarnos de que las acciones desencadenadas por el envío de nuestros datos sean confiables. Podemos proteger nuestros datos de varias formas. Lo más habitual es que enviemos los datos a través de una conexión segura TLS (Transport Layer Security). Eso garantizará que nuestros datos se encripten a través del cable. Usamos certificados para crear relaciones de confianza entre dos partes y lograr esto. En este artículo, quiero analizar el estándar JWT y ver más a fondo cómo podemos integrar JWT en una aplicación Enterprise común. En este caso, echaremos un vistazo a KumuluzEE . Echemos un vistazo a algunos conceptos básicos. JWT o JSON Web Token, o mejor aún, JavaScript Object Notation Web Token, es un estándar definido en RFC7519 . Este estándar ha sido, como todos los estándares RFC (Request For Comments), definido, escrito y publicado por el IETF (Internet Engineering Task Force). Puede definirse de múltiples formas. En general, podemos decir que JWT es una forma compacta y segura de transmitir reclamaciones entre dos partes. Una forma de simplificar lo que es una reclamación es, básicamente, describirla como un par nombre/valor que contiene información. Necesitamos esta información para garantizar algunos aspectos importantes de nuestra comunicación por Internet. Necesitamos asegurarnos de que la información que recibimos esté validada y sea confiable en primera instancia. Luego, necesitamos validarla. Básicamente, esto es todo. Para implementar este estándar, podemos usar varios marcos que pueden ayudarnos a implementar una aplicación empresarial Java. Spring Boot se usa ampliamente. Muchas veces también se envuelve bajo otro nombre en software propietario de ciertas organizaciones como bancos y otras organizaciones financieras. Para nuestro ejemplo, decidí hacer algo diferente. En lugar de Spring Boot, vamos a echar un vistazo a un ejemplo con KumuluzEE . El objetivo es identificar exactamente qué es JWT y cómo se ve. Las aplicaciones empresariales de Java son básicamente aplicaciones que se pueden implementar en un servidor de aplicaciones o simplemente ejecutarse por sí solas mediante el uso de un servidor integrado. Como ejemplo, las aplicaciones Spring Boot se ejecutan en un servidor Tomcat integrado. En este artículo, nos centraremos en KumuluzEE . Al igual que Spring Boot, también contiene un servidor integrado. Excepto que en este caso se llama Jetty. Esto se usa en combinación con Weld para proporcionar CDI (inyección de dependencia de contexto). Todos los estándares de tecnología Java EE y Jakarta EE son compatibles con este framework .

2. Ejemplo de caso


Para ejemplificar cómo funciona JWT en su forma básica, tuve que pensar en una forma de presentarlo. Los ejemplos clásicos en los que la seguridad es una preocupación son los bancos. Sin embargo, crear una aplicación bancaria completa para mostrar cómo funciona JWT sería una pérdida de tiempo y tal vez se involucrarían demasiados conceptos. En cambio, lo que hice es un sistema bancario muy simple. Nuestra principal preocupación es mostrar cómo fluyen los datos a través de la red y cómo los usuarios obtienen acceso a ciertas áreas de nuestra aplicación. Tampoco voy a hablar de TLS o de cómo podemos enviar información cifrada a través de la red. Nos centraremos en JWT en su forma más pura. Nuestro caso es un sistema bancario utilizado por un grupo que defiende la naturaleza y el medio ambiente. Esta es solo una forma divertida de mostrar cómo funciona JWT . El personaje principal de esta Liga de la Naturaleza es Lucy, que se está convirtiendo en un personaje común en todos mis artículos.

3. Arquitectura

Antes de comenzar, dibujemos un boceto de nuestra aplicación en ejecución. Es una aplicación muy simple, pero aun así es bueno dibujarla:

La razón por la que esto es tan simple es que, dado que JWT se verifica en cada solicitud y cada solicitud se verifica con la clave pública, entonces sabemos que, siempre que enviemos el token correcto en cada solicitud, podremos pasar. JWT se puede integrar con OAuth2, Okta SSO o cualquier otro mecanismo de autorización. En este caso, lo que estamos haciendo es establecer la autenticación y la autorización. En nuestra aplicación, vamos a utilizar JWT y, con él, autenticar nuestro mensaje mediante una firma. Sin embargo, no iniciaremos sesión en la aplicación. En su lugar, autorizaremos a los usuarios a utilizar nuestra aplicación después de una autenticación exitosa. En este punto, es fácil ver que JWT en su núcleo es en realidad una parte muy pequeña de una aplicación completa. No obstante, es necesario agregar algunas funciones. Estos son los recursos que necesitamos:

  • Sistema de equilibrio
  • Sistema de crédito


Digamos que nuestro sistema básico solo registrará solicitudes de dinero y crédito. Básicamente, solo acumulará valores. Supongamos también que algunas personas podrán obtener crédito y otras no. Algunas personas podrán almacenar dinero y otras podrán obtener crédito.

4. Elección de tecnologías

Como se mencionó en la introducción, utilizaremos KumuluzEE como nuestro marco de aplicación empresarial e implementaremos una aplicación ultrabásica de manera que podamos ver la terminología y los conceptos básicos JWT . Asegúrese de tener la versión correcta de Java. En esta etapa, necesitaremos tener instalado el SDK de Java 17 como mínimo. Necesitaremos maven, git, un IDE compatible con Java como IntelliJ y algún tipo de shell.

5. Configuración

Para comenzar nuestra aplicación, tenemos algunas dependencias KumuluzEE . Esto se debe principalmente a que KumuluzEE , al igual que Spring Boot, necesita un par de dependencias. Echemos un vistazo al archivo POM brevemente:

 <dependencies> <dependency> <groupId>com.kumuluz.ee.openapi</groupId> <artifactId>kumuluzee-openapi-mp</artifactId> </dependency> <dependency> <groupId>com.kumuluz.ee.openapi</groupId> <artifactId>kumuluzee-openapi-mp-ui</artifactId> </dependency> <dependency> <groupId>com.kumuluz.ee</groupId> <artifactId>kumuluzee-microProfile-3.3</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.mockk</groupId> <artifactId>mockk-jvm</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.ninja-squad</groupId> <artifactId>springmockk</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.kotest</groupId> <artifactId>kotest-assertions-core-jvm</artifactId> <scope>test</scope> </dependency> </dependencies>

Analicemos brevemente algunas dependencias. Mientras lee esto, siga nuestro archivo pom.xml de arriba a abajo. Esto es importante para comprender la siguiente explicación. Necesitamos un paquete de dependencias para que nuestra aplicación funcione. Afortunadamente, KumuluzEE nos proporciona bibliotecas Microprofile que contienen paquetes estándar básicos para iniciar esta aplicación. Todo esto está contenido en la biblioteca KumuluzEE -Microprofile. Para poder configurar nuestra aplicación con todos los parámetros JWT que necesitamos, tenemos que agregarle una biblioteca MicroProfile. Al mismo tiempo, necesitamos una biblioteca de procesamiento JSON. Esto será lo que hará Johnson Core. Por supuesto, necesitamos el núcleo de KumuluzEE para funcionar. Jetty es el servidor subyacente que ejecuta el marco KumuluzEE . Es por eso que lo necesitamos en nuestras dependencias. Considerando que necesitamos CDI , también necesitamos una biblioteca que lo admita. Para habilitar nuestros puntos finales REST, necesitamos la biblioteca rest de KumuluzEE . Para obtener nuestra API, necesitamos una biblioteca Geronimo. Esto garantizará que tengamos una implementación de JSR-374 disponible. También necesitamos interpretar nuestro JWT y su contenido JSON-formatted Lombok no es realmente necesario per se. ¡Solo hace que todo sea hermoso y brillante! Logback también es importante para que podamos interpretar mejor los registros y comprender nuestros resultados. Ahora echemos un vistazo a nuestra carpeta resources . Para comenzar, primero entendamos qué esperamos encontrar en esta carpeta. Necesitamos configurar nuestra aplicación con algo relacionado con JWT , Logback y, finalmente, necesitamos decir algo sobre los beans que vamos a crear. Veamos el archivo más simple allí. El beans.xml se puede encontrar en META-INF:

 <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" xmlns:weld="http://jboss.org/schema/weld/beans" bean-discovery-mode="all"> <weld:scan> <weld:exclude name="org.jesperancinha.fintech.model.Accounts"/> </weld:scan> </beans>

Este es un archivo típico y, como puede que estés pensando ahora, un poco antiguo. En este punto, la idea es simplemente poner en funcionamiento KumuluzEE . Tenemos una acción de exclusión. Esto le dice a Weld que no considere la clase Accounts en su acción de escaneo de beans. Esto es importante porque con la implementación que estamos usando, Weld básicamente considerará cada clase con un constructor vacío como un bean. Veremos más adelante por qué no queremos que Accounts se considere un bean. Por el momento, tengamos en cuenta que estamos realizando solicitudes en el ámbito Request. Esto es lógico porque cada solicitud puede tener un usuario diferente. Veamos ahora cómo se implementa " logback ". También se encuentra en META-INF :

 <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> </root> </configuration>

Esta es una configuración muy sencilla para nuestros logs . Por último, quizás el archivo más importante de nuestra aplicación. Se trata de la plantilla de configuración. En este punto, es importante tener en cuenta que algunos de los archivos que he creado en este proyecto son parte de una estructura de plantilla. Explicaré más sobre esto más adelante. Se supone que este archivo de plantilla se debe convertir en un archivo config.yml que será leído por MicroProfile. Este archivo se encuentra en la raíz de los recursos:

 kumuluzee: name: your-financeje-banking version: 1.0.0 jwt-auth: public-key: {{ publicKey }} issuer: {{ issuer }} healthy: true

Más adelante veremos qué significan exactamente todas estas propiedades. Todas se explican por sí solas. La clave pública y el emisor son parámetros que se reemplazarán. Lo exploraremos más adelante. Nuestros scripts bash se asegurarán de que se reemplacen. Ya casi estamos listos para comenzar a codificar, pero primero, echemos un vistazo a la estructura de nuestro token JWT .

6. Código práctico

Vamos a crear nuestra aplicación muy pequeña. En esta sección, explicaremos cómo podemos hacer que nuestra aplicación funcione con JWT . Lo que queremos ver es si podemos especificar que los usuarios accedan a algunos de nuestros métodos REST y no a otros. Una de las formas de comenzar a analizar este código es echar un vistazo a nuestro token JWT simple. Aquí está nuestro ejemplo de administrador:

 { "iss": "joaofilipesabinoesperancinha", "jti": "01MASTERFINANCE", "sub": "admin", "aud": "nature", "upn": "admin", "groups": [ "user", "admin", "client", "credit" ], "user_id": 1, "access": "TOP", "name": "Admin" }

Cada uno de estos nombres en nuestro JSON se denomina reclamaciones. En nuestro ejemplo, vemos algunas reclamaciones reservadas:

  • " iss ": este es el emisor del token. Podemos elegir un valor arbitrario para este parámetro. El valor de este parámetro debe coincidir con la variable del emisor que se reemplazará en el archivo config.yml que hemos visto antes.
  • " jti ": este es un identificador único del token. Podemos usar esta afirmación, por ejemplo, para evitar que un token se use dos o más veces.
  • " sub ": este es el sujeto del token. Puede ser el usuario o cualquier otra cosa que queramos. Es importante tener en cuenta que también se puede utilizar como identificador, clave, nombre o cualquier cosa que queramos.
  • " upn ": nombre principal del usuario. Se utiliza para identificar el principal que utiliza el usuario.
  • " groups ": es una matriz de los grupos a los que pertenece el usuario actual. Básicamente, esto determinará lo que puede hacer una solicitud con este token. En nuestro token, vemos algunas reclamaciones personalizadas. Podemos usarlas de la misma manera que las reclamaciones reservadas.
  • " user_id ": usaremos esto para establecer el ID del usuario.
  • " access " — Determinaremos el nivel de acceso del usuario.
  • " name " — El nombre del usuario.

7. Código práctico

Hagamos un resumen de lo que sabemos hasta ahora. Sabemos que nos comunicaremos con tokens con una estructura que hemos determinado. Además, hemos configurado nuestra aplicación, la configuración de logback y, por último, configuramos una configuración personalizada para la búsqueda de enterprise bean. Veamos el modelo de paquete. Aquí encontraremos 3 clases. Estas clases básicamente representan una agregación de cuentas y la representación entre client y account . De esta manera, podemos comenzar mirando el archivo kotlin Model.kt donde se encuentra Client :

 data class Client constructor( @JsonProperty var name: String ?= null )

Esta primera clase modelo es la representación de nuestro cliente. Nuestro client para nuestro caso solo tiene un nombre. Este es el nombre de usuario representado por el atributo " jwt ". Además, tenemos Account :

 data class Account( @JsonProperty val accountNumber: String?, @JsonProperty val client: Client? = null, @JsonProperty var currentValue: BigDecimal = BigDecimal.ZERO, @JsonProperty var creditValue: BigDecimal = BigDecimal.ZERO ) { fun addCurrentValue(value: Long) = Account( accountNumber, client, currentValue .add(BigDecimal.valueOf(value)), creditValue ) fun addCreditValue(value: Long): Account = Account( accountNumber, client, currentValue, currentValue .add(BigDecimal.valueOf(value)) ) }


En esta clase, básicamente configuramos un accountNumber, un client, un currentValue y finalmente un creditValue. Observe que estamos estableciendo todos los valores en 0 de forma predeterminada. También estamos usando BigDecimal, simplemente porque estamos tratando con dinero. El dinero debe ser exacto y no puede sufrir redondeos del sistema hacia arriba o hacia abajo. Esto significa, en otras palabras y como ejemplo, que un número como 0000000000000000000000000000000000000000000000000001 euros debe seguir siendo ese número todo el tiempo. Además, queremos agregar valores a nuestra cuenta. Aquí es donde entra en juego el método addCurrentValue. Por las mismas razones, también recargaremos nuestro crédito con addCreditValue . Finalmente, en la última parte de nuestra configuración de datos nos encontramos con la clase Accounts :

 open class Accounts constructor( open val accountMap: MutableMap<String, Account> = mutableMapOf() )

En esencia, se trata de un agregador de todas nuestras cuentas. Usaremos el contenido de su mapa para imitar el comportamiento de una base de datos. Ahora, veamos el paquete del controlador. Aquí es donde creamos nuestra aplicación que se ejecuta con nuestro modelo de datos. Primero, echemos un vistazo a la clase BankApplication :

 @LoginConfig(authMethod = "MP-JWT") @ApplicationPath("/") @DeclareRoles("admin", "creditor", "client", "user") class BankApplication : Application()


Con esto, estamos diciendo 3 cosas importantes. Con la anotación LoginConfig, la definimos para usar y entender los tokens JWT según MicroProfile. La ApplicationPath define la raíz de la aplicación. Aquí es donde comenzará la URL de la aplicación. En nuestro ejemplo, será HTTP://localhost:8080 . Finalmente, DeclareRoles define los roles que serán utilizados y aceptados por nuestra aplicación. Roles y Grupos son términos intercambiables en esta situación. Para que la inyección funcione de manera eficiente, creamos una anotación específica para identificar el mapa de cuentas:

 annotation class AccountsProduct

Entrar al modo de pantalla completa Salir del modo de pantalla completa

A continuación, creamos una fábrica de objetos de caché AccountsFactory:

 class AccountsFactory : Serializable { @Produces @AccountsProduct @ApplicationScoped fun accounts(): Accounts = Accounts(mutableMapOf()) companion object { @Throws(JsonProcessingException::class) fun createResponse( currentAccount: Account, name: JsonString, accounts: Accounts, log: Logger, objectMapper: ObjectMapper, principal: Principal?, jsonWebToken: JsonWebToken? ): Response { val jsonObject = Json.createObjectBuilder() .add("balance", currentAccount.currentValue) .add("client", name) .build() accounts.accountMap[name.string] = currentAccount log.info("Principal: {}", objectMapper.writeValueAsString(principal)) log.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } } }


Esta fábrica es la razón por la que deshabilitamos la búsqueda específicamente para Accounts . En lugar de permitir que el proceso de búsqueda cree un bean, creamos la instancia del agregador nosotros mismos. El uso de la anotación Produces nos permite crear el bean. Al usar nuestra anotación personalizada, AccountsProduct, hacemos que el uso de este bean sea más específico. Finalmente, al usar ApplicationScoped , definimos su alcance como el alcance Application . En otras palabras, el bean de agregación de cuentas se comportará como un objeto singleton en toda la aplicación. " createResponse " es solo un método genérico para crear respuestas JSON. Lo que necesitamos ahora son dos "Resources". Esto es básicamente lo mismo que " Controllers " en Spring. Es un nombre diferente, pero tiene exactamente el mismo uso. Veamos la clase AccountsResource :

 @Path("accounts") @RequestScoped @Produces(MediaType.APPLICATION_JSON) open class AccountResource { @Inject @AccountsProduct open var accounts: Accounts? = null @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null @Inject @Claim("access") open var access: JsonString? = null @Claim("iat") @Inject open var iat: JsonNumber? = null @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null @POST @RolesAllowed("admin", "client", "credit") @Throws(JsonProcessingException::class) open fun createAccount(): Response = createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) @POST @RolesAllowed("admin", "user") @Path("user") @Throws(JsonProcessingException::class) open fun createUser(): Response { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) } @GET @RolesAllowed("admin", "client") @Throws(JsonProcessingException::class) open fun getAccount(): Response? { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() ) } @PUT @RolesAllowed("admin", "client") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody): Response? { val userAccount = requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0) requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount return createResponse(currentAccount) } @GET @Path("all") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getAll(): Response? { val allAccounts = ArrayList( requireNotNull(accounts).accountMap .values ) logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(allAccounts) .build() } @GET @Path("summary") @Throws(JsonProcessingException::class) open fun getSummary(): Response? { val totalCredit = requireNotNull(accounts).accountMap .values .map(Account::currentValue) .stream() .reduce { result, u -> result.add(u) } .orElse(BigDecimal.ZERO) val jsonObject = Json.createObjectBuilder() .add("totalCurrent", totalCredit) .add("client", "Mother Nature Dream Team") .build() logger.info("Summary") logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } @GET @RolesAllowed("admin", "client") @Path("jwt") open fun getJWT(): Response? { val jsonObject = Json.createObjectBuilder() .add("jwt", requireNotNull(jsonWebToken).rawToken) .add("userId", requireNotNull(userId).doubleValue()) .add("access", requireNotNull(access).string) .add("iat", requireNotNull(iat).doubleValue()) .build() return Response.ok(jsonObject) .build() } @Throws(JsonProcessingException::class) private fun createResponse(currentAccount: Account): Response = AccountsFactory.createResponse( currentAccount, requireNotNull(name), requireNotNull(accounts), logger, objectMapper, principal, jsonWebToken ) companion object { val objectMapper: ObjectMapper = ObjectMapper() val logger: Logger = LoggerFactory.getLogger(AccountResource::class.java) } }

Tómese un momento para observar esta clase con más detalle. La anotación Path define cómo llegar a este recurso desde la raíz. Recuerde que estamos usando "/" como raíz. En este caso, "accounts" es nuestro punto de acceso raíz para este recurso. Todos nuestros recursos, en nuestro caso solo dos, se ejecutan con el alcance RequestResource. Con la anotación Produces se determina que todas las respuestas a todas las solicitudes, independientemente de su tipo, tomarán la forma de mensajes con formato JSON. Para inyectar nuestro aggregator solo usamos la combinación de la anotación Inject y la anotación AccountsProduct :

 @Inject @AccountsProduct open var accounts: Accounts? = null


Esto coincide con lo que definimos en la fábrica. Además, también estamos inyectando dos elementos importantes de seguridad: un principal y el jsonWebToken :

 @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null


Tanto JsonWebToken como Principal serán iguales, y lo veremos en nuestros registros. En nuestros recursos, siempre podemos inyectar reclamos desde una solicitud con un token determinado:

 @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null


Esto se logra con la combinación de las anotaciones Inject y Claim . El nombre colocado debajo de la anotación Claim define qué claim queremos inyectar. Debemos tener cuidado con el tipo con el que definimos nuestros parámetros. En nuestro ejemplo, solo necesitamos los tipos JsonString y JsonNumber Primero, veamos cómo estamos creando cuentas y usuarios:

 @POST @RolesAllowed("admin", "client", "credit") @Throws(JsonProcessingException::class) open fun createAccount(): Response = createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) @POST @RolesAllowed("admin", "user") @Path("user") @Throws(JsonProcessingException::class) open fun createUser(): Response { return createResponse( requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account( client = Client(name = requireNotNull(name).string), accountNumber = UUID.randomUUID().toString() ) ) }

Creación de cuentas y usuarios


El objetivo aquí es poder separar los métodos y darles diferentes permisos. En nuestro ejemplo, ambos solo crean una cuenta, pero es importante notar que solo los usuarios con roles de usuario pueden usar el método createUser. De la misma manera, solo los usuarios con roles de cliente y crédito pueden acceder al método createAccount. Veamos ahora en detalle el método de solicitud PUT de este recurso:

 @PUT @RolesAllowed("admin", "client") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody): Response? { val userAccount = requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError() .build() val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0) requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount return createResponse(currentAccount) }

Cobrando


Sabemos que la anotación PUT indica que este método solo es accesible con solicitudes de tipo PUT . La anotación Path le indica a Jetty que la ruta a este método es un valor. Esto también se conoce como PathParam . Finalmente, podemos definir que este método solo pueda ser utilizado por usuarios con roles de administrador o cliente. El valor de entrada se pasa a nuestra variable de valor Long mediante el uso de PathParam. Si no definimos ningún rol, cualquier usuario con el token correcto podrá acceder a estos métodos. CreditResource se implementa de la misma manera:

 @Path("credit") @RequestScoped @Produces(MediaType.APPLICATION_JSON) open class CreditResource { @Inject @AccountsProduct open var accounts: Accounts? = null @Inject open var principal: Principal? = null @Inject open var jsonWebToken: JsonWebToken? = null @Inject @Claim("access") open var access: JsonString? = null @Inject @Claim("iat") open var iat: JsonNumber? = null @Inject @Claim("name") open var name: JsonString? = null @Inject @Claim("user_id") open var userId: JsonNumber? = null @GET @RolesAllowed("admin", "credit") @Throws(JsonProcessingException::class) open fun getAccount(): Response = requireNotNull(accounts).let { accounts -> createResponse( accounts.accountMap[requireNotNull(name).string] ?: return Response.serverError().build() ) } @PUT @RolesAllowed("admin", "credit") @Consumes(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun cashIn(transactionBody: TransactionBody) = requireNotNull(accounts).let { accounts -> requireNotNull(name).let { name -> accounts.accountMap[name.string] = (accounts.accountMap[name.string] ?: return Response.serverError() .build()).addCreditValue(transactionBody.saldo?: 0L) createResponse( (accounts.accountMap[name.string] ?: return Response.serverError() .build()).addCreditValue(transactionBody.saldo?: 0L) ) } } @GET @Path("all") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getAll(): Response? { val allAccounts = ArrayList( requireNotNull(accounts).accountMap .values ) logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(allAccounts) .build() } @GET @Path("summary") @Produces(MediaType.APPLICATION_JSON) @Throws( JsonProcessingException::class ) open fun getSummary(): Response? { val totalCredit = requireNotNull(accounts).accountMap .values .map(Account::creditValue) .stream() .reduce { total, v -> total.add(v) } .orElse(BigDecimal.ZERO) val jsonObject = Json.createObjectBuilder() .add("totalCredit", totalCredit) .add("client", "Mother Nature Dream Team") .build() logger.info("Summary") logger.info("Principal: {}", objectMapper.writeValueAsString(principal)) logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken)) return Response.ok(jsonObject) .build() } @GET @RolesAllowed("admin", "client") @Path("jwt") open fun getJWT(): Response? { val jsonObject = Json.createObjectBuilder() .add("jwt", requireNotNull(jsonWebToken).rawToken) .add("userId", requireNotNull(userId).doubleValue()) .add("access", requireNotNull(access).string) .add("iat", requireNotNull(iat).doubleValue()) .build() return Response.ok(jsonObject) .build() } @Throws(JsonProcessingException::class) private fun createResponse(currentAccount: Account): Response { return AccountsFactory.createResponse( currentAccount, requireNotNull(name), requireNotNull(accounts), logger, objectMapper, principal, jsonWebToken ) } companion object { val objectMapper: ObjectMapper = ObjectMapper() val logger: Logger = LoggerFactory.getLogger(CreditResource::class.java) } }

La única diferencia es que en lugar de usar los roles admin y client , ahora usamos los roles admin y credit . Además, tenga en cuenta que las cuentas de los usuarios nunca se crearán en este resource . Eso solo es posible a través del resource de la cuenta. Ahora que sabemos cómo se implementa el código, primero recapitulemos qué métodos hemos puesto a disposición en nuestro servicio REST .

8. Uso de la aplicación

Veamos la lista de los servicios que se utilizan:


Tipo, URL, Carga útil, Resultado, Roles permitidos
CORREO, http://localhost:8080/accounts,n/a,Creado cuenta,administrador/cliente/crédito
CORREO,
http://localhost:8080/accounts/user,n/a,Creado usuario,admin/usuario
CONSEGUIR,
http://localhost:8080/accounts,n/a,Coincidencia cuenta,administrador/cliente
PONER,
http://localhost:8080/cuentas,{saldo: Largo}, Saldo actual, admin/cliente
CONSEGUIR,
http://localhost:8080/accounts/all,n/a,Todos cuentas corrientes,Todo
CONSEGUIR,
http://localhost:8080/accounts/summary,n/a,Suma de todos los saldos,Todos
CONSEGUIR,
http://localhost:8080/credit,n/a,Coincidencia cuenta,administrador/cliente
PONER,
http://localhost:8080/credit,{saldo: Largo}, Crédito actual, admin/cliente
CONSEGUIR,
http://localhost:8080/credit/all,n/a,Todos créditos,Todos
CONSEGUIR,
http://localhost:8080/credit/summary,n/a,Suma
créditos,Todos

9. Generación del entorno de pruebas

He creado un archivo bash en la carpeta raíz. Este archivo se llama "setupCertificates.sh". Echémosle un vistazo para tener una idea de lo que hace:

 #!/bin/bash mkdir -p your-finance-files cd your-finance-files || exit openssl genrsa -out baseKey.pem openssl pkcs8 -topk8 -inform PEM -in baseKey.pem -out privateKey.pem -nocrypt openssl rsa -in baseKey.pem -pubout -outform PEM -out publicKey.pem echo -e '\033[1;32mFirst test\033[0m' java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \ -p ../jwt-plain-tokens/jwt-token-admin.json \ -key ../your-finance-files/privateKey.pem >> token.jwt CERT_PUBLIC_KEY=$(cat ../your-finance-files/publicKey.pem) CERT_ISSUER="joaofilipesabinoesperancinha" echo -e "\e[96mGenerated public key: \e[0m $CERT_PUBLIC_KEY" echo -e "\e[96mIssued by: \e[0m $CERT_ISSUER" echo -e "\e[96mYour token is: \e[0m $(cat token.jwt)" cp ../your-financeje-banking/src/main/resources/config-template ../your-financeje-banking/src/main/resources/config_copy.yml CERT_CLEAN0=${CERT_PUBLIC_KEY//"/"/"\/"} CERT_CLEAN1=${CERT_CLEAN0//$'\r\n'/} CERT_CLEAN2=${CERT_CLEAN1//$'\n'/} CERT_CLEAN3=$(echo "$CERT_CLEAN2" | awk '{gsub("-----BEGIN PUBLIC KEY-----",""); print}') CERT_CLEAN4=$(echo "$CERT_CLEAN3" | awk '{gsub("-----END PUBLIC KEY-----",""); print}') CERT_CLEAN=${CERT_CLEAN4//$' '/} echo -e "\e[96mCertificate cleanup: \e[0m ${CERT_CLEAN/$'\n'/}" sed "s/{{ publicKey }}/$CERT_CLEAN/g" ../your-financeje-banking/src/main/resources/config_copy.yml > ../your-financeje-banking/src/main/resources/config_cert.yml sed "s/{{ issuer }}/$CERT_ISSUER/g" ../your-financeje-banking/src/main/resources/config_cert.yml > ../your-financeje-banking/src/main/resources/config.yml rm ../your-financeje-banking/src/main/resources/config_cert.yml rm ../your-financeje-banking/src/main/resources/config_copy.yml echo -e "\e[93mSecurity elements completely generated!\e[0m" echo -e "\e[93mGenerating tokens...\e[0m" TOKEN_FOLDER=jwt-tokens mkdir -p ${TOKEN_FOLDER} # CREATE_ACCOUNT_FILE=createAccount.sh CREATE_USER_FILE=createUser.sh SEND_MONEY_FILE=sendMoney.sh ASK_CREDIT_FILE=askCredit.sh TOKEN_NAME_VALUE=tokenNameValue.csv echo "#!/usr/bin/env bash" > ${CREATE_ACCOUNT_FILE} chmod +x ${CREATE_ACCOUNT_FILE} echo "#!/usr/bin/env bash" > ${CREATE_USER_FILE} chmod +x ${CREATE_USER_FILE} echo "#!/usr/bin/env bash" > ${SEND_MONEY_FILE} chmod +x ${SEND_MONEY_FILE} echo "#!/usr/bin/env bash" > ${ASK_CREDIT_FILE} chmod +x ${ASK_CREDIT_FILE} for item in ../jwt-plain-tokens/jwt-token*.json; do if [[ -f "$item" ]]; then filename=${item##*/} per_token=${filename/jwt-token-/} token_name=${per_token/.json/} cp "${item}" jwt-token.json java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \ -p jwt-token.json \ -key ../your-finance-files/privateKey.pem > token.jwt cp token.jwt ${TOKEN_FOLDER}/token-"${token_name}".jwt token=$(cat token.jwt) echo "# Create account: ""${token_name}" >> ${CREATE_ACCOUNT_FILE} echo "echo -e \"\e[93mCreating account \e[96m${token_name}\e[0m\"" >> ${CREATE_ACCOUNT_FILE} echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X POST >> ${CREATE_ACCOUNT_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_ACCOUNT_FILE} echo "# Create user: ""${token_name}" >> ${CREATE_USER_FILE} echo "echo -e \"\e[93mCreating user \e[96m${token_name}\e[0m\"" >> ${CREATE_USER_FILE} echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts/user -X POST >> ${CREATE_USER_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_USER_FILE} echo "# Send money to: "${token_name} >> ${SEND_MONEY_FILE} echo "echo -e \"\e[93mSending money to \e[96m${token_name}\e[0m\"" >> ${SEND_MONEY_FILE} echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'" >> ${SEND_MONEY_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${SEND_MONEY_FILE} echo "# Asking money credit to: "${token_name} >> ${ASK_CREDIT_FILE} echo "echo -e \"\e[93mAsking credit from \e[96m${token_name}\e[0m\"" >> ${ASK_CREDIT_FILE} echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/credit -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'">> ${ASK_CREDIT_FILE} echo "echo -e \"\e[93m\n---\e[0m\"" >> ${ASK_CREDIT_FILE} echo "${token_name},${token}" >> ${TOKEN_NAME_VALUE} fi done

Generación de ambiente

Siga el archivo mientras explico lo que hace. Esto es importante para que entendamos exactamente lo que está haciendo. Primero creamos claves privadas y públicas en formato PEM . Luego usamos la clave privada con nuestro ejecutable "your-finance-jwt-generator.jar". Este es nuestro jar ejecutable que permite la creación rápida de tokens. El emisor no se puede cambiar más adelante. Finalmente, crea un token. Veremos cómo leer este token más adelante. Este token contiene 3 reclamos de encabezado adicionales. Estos son "kid", "typ" y "alg". Sigue el siguiente formato:

 { "kid": "jwt.key", "typ": "JWT", "alg": "RS256" }

El encabezado del JWT

Veamos estas afirmaciones más de cerca:

  • "kid": funciona como una pista que indica qué tipo de algoritmo estamos usando.
  • "typ" — Se utiliza para declarar los tipos de medios IANA . Hay tres opciones: JWT (token web JSON), JWE (cifrado web JSON) y JWA (algoritmos web JSON). Estos tipos no son relevantes para nuestro experimento. Solo veremos que nuestro token no está muy bien cifrado y que es muy fácil descifrarlo. También veremos que, aunque podemos descifrar tokens, no podemos manipularlos tan fácilmente para realizar otras acciones.
  • "alg": así definimos el tipo de firma que queremos utilizar. La firma puede considerarse una operación criptográfica que garantizará que el token original no haya sido modificado y sea confiable. En nuestro caso, utilizamos RS256, también conocido como Firma RSA con SHA-256.

Con nuestra clave pública, finalmente podemos usarla para cambiar nuestra plantilla. El nuevo archivo config.yml debería verse así:

 kumuluzee: name: your-financeje-banking version: 1.0.0 jwt-auth: public-key: FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKEN issuer: joaofilipesabinoesperancinha healthy: true

configuración.yml


El segundo paso es crear cuatro archivos. Para cada token simple en el directorio " jwt-plain-tokens ", crearemos cuatro comandos. El primer comando es para crear usuarios que puedan hacer cosas de manera efectiva con sus cuentas. Estos son usuarios con perfiles " admin ", " client " y " credit ". Ejecutemos el archivo " createAccount.sh " para crearlos. El segundo comando creará el resto de los usuarios que aún no poseen ningún derecho. Este es el archivo "createUser.sh". Ejecutémoslo. Ahora veremos que finalmente se crearon todos los usuarios. Ahora veamos los detalles sobre las transacciones y observemos los dos comandos restantes. Uno para "cashin" y otro para solicitar más crédito. El primer archivo generado es el script bash "sendMoney.sh". Aquí podemos encontrar todas las solicitudes para " cashin ". En este archivo encontrará una solicitud curl para enviar cantidades aleatorias de dinero a los usuarios, por usuario. Veamos el caso de administrador:

 #!/usr/bin/env bash # Send money to: admin echo -e "\e[93mSending money to \e[96madmin\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer= FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 125}' echo -e "\e[93m\n---\e[0m" # Send money to: cindy echo -e "\e[93mSending money to \e[96mcindy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 360}' echo -e "\e[93m\n---\e[0m" # Send money to: faustina echo -e "\e[93mSending money to \e[96mfaustina\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 50}' echo -e "\e[93m\n---\e[0m" # Send money to: jack echo -e "\e[93mSending money to \e[96mjack\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 205}' echo -e "\e[93m\n---\e[0m" # Send money to: jitska echo -e "\e[93mSending money to \e[96mjitska\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 332}' echo -e "\e[93m\n---\e[0m" # Send money to: judy echo -e "\e[93mSending money to \e[96mjudy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 295}' echo -e "\e[93m\n---\e[0m" # Send money to: lucy echo -e "\e[93mSending money to \e[96mlucy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 160}' echo -e "\e[93m\n---\e[0m" # Send money to: malory echo -e "\e[93mSending money to \e[96mmalory\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 413}' echo -e "\e[93m\n---\e[0m" # Send money to: mara echo -e "\e[93mSending money to \e[96mmara\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 464}' echo -e "\e[93m\n---\e[0m" # Send money to: namita echo -e "\e[93mSending money to \e[96mnamita\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 51}' echo -e "\e[93m\n---\e[0m" # Send money to: pietro echo -e "\e[93mSending money to \e[96mpietro\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 491}' echo -e "\e[93m\n---\e[0m" # Send money to: rachelle echo -e "\e[93mSending money to \e[96mrachelle\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 474}' echo -e "\e[93m\n---\e[0m" # Send money to: sandra echo -e "\e[93mSending money to \e[96msandra\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 417}' echo -e "\e[93m\n---\e[0m" # Send money to: shikka echo -e "\e[93mSending money to \e[96mshikka\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 64}' echo -e "\e[93m\n---\e[0m"

Extracto de sendMoney.sh

Los mismos usuarios también tienen asignadas sus solicitudes de crédito:

 #!/usr/bin/env bash # Asking money credit to: admin echo -e "\e[93mAsking credit from \e[96madmin\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 137}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: cindy echo -e "\e[93mAsking credit from \e[96mcindy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 117}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: faustina echo -e "\e[93mAsking credit from \e[96mfaustina\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 217}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: jack echo -e "\e[93mAsking credit from \e[96mjack\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 291}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: jitska echo -e "\e[93mAsking credit from \e[96mjitska\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 184}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: judy echo -e "\e[93mAsking credit from \e[96mjudy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 388}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: lucy echo -e "\e[93mAsking credit from \e[96mlucy\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 219}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: malory echo -e "\e[93mAsking credit from \e[96mmalory\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 66}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: mara echo -e "\e[93mAsking credit from \e[96mmara\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 441}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: namita echo -e "\e[93mAsking credit from \e[96mnamita\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 358}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: pietro echo -e "\e[93mAsking credit from \e[96mpietro\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 432}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: rachelle echo -e "\e[93mAsking credit from \e[96mrachelle\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 485}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: sandra echo -e "\e[93mAsking credit from \e[96msandra\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 500}' echo -e "\e[93m\n---\e[0m" # Asking money credit to: shikka echo -e "\e[93mAsking credit from \e[96mshikka\e[0m" curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 89}' echo -e "\e[93m\n---\e[0m"

Extracto de askCredit.sh



Todos nuestros characters son parte de la Liga de Nature . Básicamente, solo un grupo de personas que forman parte de este sistema bancario. En este contexto, están defendiendo el medio ambiente. No es realmente relevante para el artículo lo que hace este grupo de personas o en qué parte de la historia encajan, pero para el contexto, participan en acciones para defender el medio ambiente y frenar los efectos del cambio climático . Algunos de nuestros characters pueden hacer todo, otros no pueden hacer nada y otros solo pueden "cobrar" o simplemente "pedir crédito". También tenga en cuenta que estoy ocultando información confidencial. Estos tokens normalmente no deberían compartirse ni ser visibles en una URL en particular. Sí, siempre están disponibles a través de la consola de desarrollador del navegador, pero de todos modos es para protect algunas solicitudes que se realizan. Este es un concepto conocido como "seguridad por oscuridad" and aunque técnicamente no impide que el usuario se dé cuenta del token que está utilizando, sí funciona como elemento disuasorio. En ambos métodos, cuando hacemos un depósito o cuando pedimos crédito, note que por cada solicitud estamos enviando un número aleatorio entre 1 y 500. Ya casi estamos listos para iniciar nuestra aplicación, pero primero, profundicemos un poco más en la teoría.

10. ¿Cómo se crea un token JWT




Ahora que hemos generado nuestros tokens, veamos uno de ellos. Voy a mostrarte un token ofuscado y lo vamos a usar para entender esto. Aquí está nuestro token: FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE . FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO . FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKEN Lo que es importante notar aquí es que nuestro token se divide en tres partes:

  • Encabezado : este es un encabezado de configuración JSON codificado en Base64, tal como lo comentamos anteriormente.
  • Carga útil : esta es una carga útil JSON codificada en Base64. Aquí es donde definimos nuestras reclamaciones reservadas y personalizadas. También podemos definir aquí reclamaciones privadas y públicas. Ambas caen dentro de las reclamaciones personalizadas. Como nota rápida, podemos hacer lo que queramos con ambas reclamaciones. Sin embargo, las reclamaciones públicas se denominan como las definidas en el Registro de tokens web JSON de la IANA. Es importante que nombremos nuestros tokens de una determinada manera para evitar colisiones con el registro. Las reclamaciones públicas también se pueden definir como opcionales estándar. Las reclamaciones privadas no siguen ningún estándar y depende de nosotros definirlas.
  • Firma : aquí es donde podemos ser un poco creativos. La firma es una combinación cifrada del Header y la Payload . Decidimos el algoritmo que queremos utilizar y este bit del token determinará básicamente si el mensaje que estamos enviando es confiable. Es exclusivo de esa combinación y nuestro servidor utilizará la "clave pública" que creamos para determinar si tenemos una coincidencia. Si recuerdas lo anterior, estamos usando RS256 en nuestro ejemplo.


Antes de continuar, tenga en cuenta que tanto el Header como la Payload se pueden decyphered en nuestro ejemplo. Simplemente "no podemos" manipular la carga útil o el encabezado y aún así hacer que sea confiable. La protección contra los posibles efectos de un token malicioso solo se puede proteger mediante el algoritmo que elijamos. Así que elija sabiamente. Si trabaja en una organización donde la información de alto secreto es una preocupación, como un banco, NO haga lo que estamos a punto de hacer. Esta es solo una forma de verificar en línea el contenido de los tokens que hemos generado localmente. Primero, vayamos a https://jwt.io/ y completemos nuestro token JWT . Use el token que acaba de generar:


Usando https://jwt.io/ para verificar el contenido de nuestro token Examinemos lo que tenemos aquí. Este es nuestro token de administrador. Esa persona es "Admin" en nuestro ejemplo. Podemos ver que nuestros parámetros están todos disponibles. En nuestra lista vemos "sub", "aud", "upn", "access", "user_id", "iss", "name", "groups" y finalmente "jti". También tenemos algunas reclamaciones adicionales. Veámoslas:



" auth_time " — Esta es la fecha en la que se realizó la autenticación. Nuestro token se autenticó el domingo 17 de julio de 2022 a las 16:15:47 GMT+02:00 DST" iat " — Esta es la fecha en la que se creó el token. En nuestro caso, esto sucede simultáneamente con auth_time." exp " — Esta es la fecha de vencimiento del token. Caduca el domingo 17 de julio de 2022 a las 16:32:27 GMT+02:00 DST. No especificamos ninguna fecha de vencimiento en nuestro token. Esto significa que JWT usa su valor predeterminado de ~15 minutos.

Ahora vamos a realizar algunas pruebas.

11. Ejecución de la aplicación

El código está listo para usarse en GitHub . Si verificamos el código y lo abrimos con Intellij, debemos tener en cuenta que no podemos ejecutar esta aplicación como una aplicación Spring Boot. No hay ningún "psvm" para ejecutarlo. En cambio, podemos ejecutar el jar generado directamente y asegurarnos de hacer un "mvn build" justo antes. Así es como lo estoy usando en este momento:

[ ] https://github.com/jesperancinha/your-finance-je "Configuración del entorno para ejecutar la aplicación")



Ahora ejecutemos nuevamente el script " setupCertificates.sh ". No sé cuánto tiempo te tomó llegar hasta aquí, pero es muy probable que a esta altura ya hayan pasado los 15 minutos. Por las dudas, ejecútalos nuevamente. ¡Iniciemos nuestra aplicación! Podemos iniciarla de esta manera:

 mvn clean install java -jar your-financeje-banking/target/your-financeje-banking.jar

O podemos simplemente ejecutarlo a través de nuestra configuración lista para usar. Revise el repositorio y el Makefile de antemano si desea comprender todo lo que hace:

 make dcup-full-action

Este script ejecutará 2 servicios. Uno en el puerto 8080 y el otro en el puerto 8081 En el puerto 8080 ejecutaremos una versión de este software que ejecuta nuestro propio código para generar tokens JWT . En el puerto 8081, ejecutaremos una versión que utiliza el generador jwtknizr creado por Adam Bien . Sin embargo, este artículo se centrará en el servicio que se ejecuta en el puerto 8080 Si lo desea, también puede ejecutar cypress con:

 make cypress-open

Esto open la consola cypress y podrás ejecutar las pruebas con el navegador que elijas. Sin embargo, las opciones del navegador aún son limitadas en esta etapa. La mayoría de las solicitudes serán solicitudes de línea de comandos proporcionadas por cypress . Por ahora, no analizaremos " cypress ". Ve a tu navegador y dirígete a esta ubicación:

http://localhost:8080/cuentas/todas

Deberíamos obtener un resultado como este:


Como podemos ver, " Malory ", " Jack Fallout " y " Jitska " no tienen crédito ni dinero. Esto se debe a que solo se les ha otorgado el grupo de usuarios. Observe también que Shikka no se le ha otorgado crédito. " Shikka " es nuestro único cliente que no tiene el crédito de grupo. Si observamos los registros, podemos ver que las operaciones exitosas tienen este formato:

 Sending money to admin HTTP/1.1 200 OK Date: Sun, 17 Jul 2022 15:01:13 GMT X-Powered-By: KumuluzEE/4.1.0 Content-Type: application/json Content-Length: 32 Server: Jetty(10.0.9) {"balance":212,"client":"Admin"}


Un 200 nos permite saber que la operación se realizó con éxito. En el caso de "Malory", "Jack Fallout" y "Jitska", ambas operaciones fallan y luego recibiremos este tipo de mensaje:

 Sending money to jitska HTTP/1.1 403 Forbidden X-Powered-By: KumuluzEE/4.1.0 Content-Length: 0 Server: Jetty(10.0.9)

Un 403 nos permite saber que nuestro token JWT ha sido validado y es confiable. Sin embargo, el usuario no puede realizar esa operación. En otras palabras, no tiene acceso al método designado.

Vamos a modificar un poco nuestros tokens. Si cambiamos algunos de los tokens del archivo sendMoney.sh, deberíamos obtener esto:

 Sending money to admin HTTP/1.1 401 Unauthorized X-Powered-By: KumuluzEE/4.1.0 WWW-Authenticate: Bearer realm="MP-JWT" Content-Length: 0 Server: Jetty(10.0.9)

Entrar al modo de pantalla completa Salir del modo de pantalla completa

Este 401 significa que nuestro token no fue validado. Significa que la clave pública que el servidor utiliza para verificar si nuestro token es confiable no encontró ninguna coincidencia. Si la clave pública no puede evaluar y validar la firma del token JWT, lo rechazará.

A modo de resumen, el encabezado y el "Payload" no están cifrados. Solo están "codificados" en base 64. Esto significa que la "Decodificación" nos permite siempre echar un vistazo al interior de lo que realmente es el payload. Si buscamos proteger nuestro payload de escuchas no autorizadas, no deberíamos utilizar el "Payload" del token para nada más que para seleccionar parámetros de identificación. El problema radica realmente cuando alguien consigue el token JWT , por ejemplo, cuando el túnel TLS se ha visto comprometido y alguien puede leer el contenido de los mensajes intercambiados. Cuando eso sucede, todavía hay otra protección. Y esta es la firma. El único capaz de validar un mensaje entrante es el servidor que contiene la clave pública. Esta clave pública, aunque pública, solo permite validar el mensaje entrante ejecutándose contra la firma y el "Header + Payload".

12. Conclusión

Hemos llegado al final de nuestra sesión. Gracias por seguirla. Podemos ver cómo los tokens JWT son compactos y mucho menos detallados que su contraparte XML, los tokens SAML . Hemos visto lo fácil que es crear y usar tokens para obtener ciertas autorizaciones necesarias para ciertos métodos y cómo llegamos allí a través de un token firmado. Sin embargo, creo que es muy importante tener una idea de cómo funciona JWT . Con suerte, con esto, te he dado una buena introducción a cómo funcionan los tokens JWT . Para tener una mejor idea de cómo funciona todo esto, te aconsejo que juegues con las pruebas cypress implementadas. Esta es una excelente manera de ver cómo se realizan las solicitudes y qué estamos probando y qué se espera. Entonces también tendrás una mejor idea de por qué algunos usuarios realizan ciertas operaciones y otros no. He colocado todo el código fuente de esta aplicación en GitHub. Espero que hayas disfrutado de este artículo tanto como yo disfruté escribiéndolo. ¡Gracias por leer!

13. Referencias