En la anterior entrada del blog, hablamos de cómo configurar una autenticación OAuth2 sencilla. Sin embargo, nuestra implementación tiene un fallo importante: estamos utilizando un almacén de tokens en memoria.
Los almacenes de tokens en memoria sólo deben utilizarse durante el desarrollo o si tu aplicación tiene un único servidor, ya que no puedes compartirlos fácilmente entre nodos y, en caso de reinicio del servidor, perderás todos los tokens de acceso que haya en él.
Spring-security-oauth2 ya tiene soporte integrado para JDBC y JWT. Sin embargo, si necesitas guardar tus tokens en algún otro lugar, tienes que crear tu propio almacén de tokens de seguridad de Spring. Por desgracia, la implementación de tal cosa no es una tarea trivial, y espero que la siguiente receta le ahorrará un par de horas de trabajo.
Empecemos por crear las dos entidades responsables de almacenar tu token de acceso y refresco, y sus respectivos repositorios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
importar lombok.Datos; importar org.springframework.datos.anotación.Id; importar org.springframework.datos.couchbase.núcleo.cartografía.Documento; importar org.springframework.seguridad.oauth2.común.OAuth2AccessToken; importar org.springframework.seguridad.oauth2.proveedor.Autenticación OAuth2; @Documento @Datos público clase CouchbaseAccessToken { @Id privado Cadena id; privado Cadena tokenId; privado OAuth2AccessToken ficha; privado Cadena authenticationId; privado Cadena nombre de usuario; privado Cadena clientId; privado Cadena autenticación; privado Cadena refreshToken; público Autenticación OAuth2 getAuthentication() { devolver SerializableObjectConverter.deserializar(autenticación); } público void setAuthentication(Autenticación OAuth2 autenticación) { este.autenticación = SerializableObjectConverter.serializar(autenticación); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@N1qlPrimaryIndexed @VerIndexado(designDoc = "couchbaseAccessToken") público interfaz CouchbaseAccessTokenRepository extiende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, Cadena> { Lista<CouchbaseAccessToken> findByClientId(Cadena clientId); Lista<CouchbaseAccessToken> findByClientIdAndUsername(Cadena clientId, Cadena nombre de usuario); Opcional<CouchbaseAccessToken> findByTokenId(Cadena tokenId); Opcional<CouchbaseAccessToken> findByRefreshToken(Cadena refreshToken); Opcional<CouchbaseAccessToken> findByAuthenticationId(Cadena authenticationId); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
importar lombok.Datos; importar org.springframework.datos.anotación.Id; importar org.springframework.datos.couchbase.núcleo.cartografía.Documento; importar org.springframework.seguridad.oauth2.común.OAuth2RefreshToken; importar org.springframework.seguridad.oauth2.proveedor.Autenticación OAuth2; @Documento @Datos público clase CouchbaseRefreshToken { @Id privado Cadena id; privado Cadena tokenId; privado OAuth2RefreshToken ficha; privado Cadena autenticación; público Autenticación OAuth2 getAuthentication() { devolver SerializableObjectConverter.deserializar(autenticación); } público void setAuthentication(Autenticación OAuth2 autenticación) { este.autenticación = SerializableObjectConverter.serializar(autenticación); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
importar org.springframework.datos.couchbase.núcleo.consulta.N1qlPrimaryIndexed; importar org.springframework.datos.couchbase.núcleo.consulta.VerIndexado; importar org.springframework.datos.couchbase.repositorio.CouchbasePagingAndSortingRepository; importar java.util.Lista; importar java.util.Opcional; @N1qlPrimaryIndexed @VerIndexado(designDoc = "couchbaseAccessToken") público interfaz CouchbaseAccessTokenRepository extiende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, Cadena> { Lista<CouchbaseAccessToken> findByClientId(Cadena clientId); Lista<CouchbaseAccessToken> findByClientIdAndUsername(Cadena clientId, Cadena nombre de usuario); Opcional<CouchbaseAccessToken> findByTokenId(Cadena tokenId); Opcional<CouchbaseAccessToken> findByRefreshToken(Cadena refreshToken); Opcional<CouchbaseAccessToken> findByAuthenticationId(Cadena authenticationId); } |
Tenga en cuenta que Autenticación OAuth2 es una interfaz, por lo que no tengo otra opción que serializar el objeto para almacenarlo en la base de datos. Aquí está la clase responsable de serializarlo/deserializarlo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
público clase SerializableObjectConverter { público estático Cadena serializar(Autenticación OAuth2 objeto) { pruebe { byte[] bytes = SerializationUtils.serializar(objeto); devolver Base64.codificarCadenaBase64(bytes); } captura(Excepción e) { e.printStackTrace(); tirar e; } } público estático Autenticación OAuth2 deserializar(Cadena objeto codificado) { pruebe { byte[] bytes = Base64.descodificarBase64(objeto codificado); devolver (Autenticación OAuth2) SerializationUtils.deserializar(bytes); } captura(Excepción e) { e.printStackTrace(); tirar e; } } |
Ahora, por fin podemos crear nuestro spring oauth2 almacén de fichas. Para ello, todo lo que necesitamos es implementar la larga lista de métodos del método org.springframework.security.oauth2.provider.token.TokenStore:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
importar org.springframework.seguridad.oauth2.común.OAuth2AccessToken; importar org.springframework.seguridad.oauth2.común.OAuth2RefreshToken; importar org.springframework.seguridad.oauth2.proveedor.Autenticación OAuth2; importar org.springframework.seguridad.oauth2.proveedor.ficha.AuthenticationKeyGenerator; importar org.springframework.seguridad.oauth2.proveedor.ficha.DefaultAuthenticationKeyGenerator; importar org.springframework.seguridad.oauth2.proveedor.ficha.TokenStore; importar java.io.UnsupportedEncodingException; importar java.matemáticas.BigInteger; importar java.seguridad.MessageDigest; importar java.seguridad.NoSuchAlgorithmException; importar java.util.*; público clase CouchbaseTokenStore implementa TokenStore { privado CouchbaseAccessTokenRepository cbAccessTokenRepository; privado CouchbaseRefreshTokenRepository cbRefreshTokenRepository; público CouchbaseTokenStore(CouchbaseAccessTokenRepository cbAccessTokenRepository, CouchbaseRefreshTokenRepository cbRefreshTokenRepository){ este.cbAccessTokenRepository = cbAccessTokenRepository; este.cbRefreshTokenRepository = cbRefreshTokenRepository; } privado AuthenticationKeyGenerator authenticationKeyGenerator = nuevo DefaultAuthenticationKeyGenerator(); @Override público Autenticación OAuth2 readAuthentication(OAuth2AccessToken accessToken) { devolver readAuthentication(accessToken.getValue()); } @Override público Autenticación OAuth2 readAuthentication(Cadena ficha) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(ficha)); si (accessToken.isPresent()) { devolver accessToken.consiga().getAuthentication(); } devolver null; } @Override público void storeAccessToken(OAuth2AccessToken accessToken, Autenticación OAuth2 autenticación) { Cadena refreshToken = null; si (accessToken.getRefreshToken() != null) { refreshToken = accessToken.getRefreshToken().getValue(); } si (readAccessToken(accessToken.getValue()) != null) { este.removeAccessToken(accessToken); } CouchbaseAccessToken cat = nuevo CouchbaseAccessToken(); cat.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); cat.setTokenId(extractTokenKey(accessToken.getValue())); cat.setToken(accessToken); cat.setAuthenticationId(authenticationKeyGenerator.extractKey(autenticación)); cat.setUsername(autenticación.isClientOnly() ? null : autenticación.getName()); cat.setClientId(autenticación.getOAuth2Request().getClientId()); cat.setAuthentication(autenticación); cat.setRefreshToken(extractTokenKey(refreshToken)); cbAccessTokenRepository.guardar(cat); } @Override público OAuth2AccessToken readAccessToken(Cadena tokenValue) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(tokenValue)); si (accessToken.isPresent()) { devolver accessToken.consiga().getToken(); } devolver null; } @Override público void removeAccessToken(OAuth2AccessToken oAuth2AccessToken) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(oAuth2AccessToken.getValue())); si (accessToken.isPresent()) { cbAccessTokenRepository.borrar(accessToken.consiga()); } } @Override público void storeRefreshToken(OAuth2RefreshToken refreshToken, Autenticación OAuth2 autenticación) { CouchbaseRefreshToken crt = nuevo CouchbaseRefreshToken(); crt.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); crt.setTokenId(extractTokenKey(refreshToken.getValue())); crt.setToken(refreshToken); crt.setAuthentication(autenticación); cbRefreshTokenRepository.guardar(crt); } @Override público OAuth2RefreshToken readRefreshToken(Cadena tokenValue) { Opcional<CouchbaseRefreshToken> refreshToken = cbRefreshTokenRepository.findByTokenId(extractTokenKey(tokenValue)); devolver refreshToken.isPresent()? refreshToken.consiga().getToken() :null; } @Override público Autenticación OAuth2 readAuthenticationForRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); devolver rtk.isPresent()? rtk.consiga().getAuthentication() :null; } @Override público void removeRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); si (rtk.isPresent()) { cbRefreshTokenRepository.borrar(rtk.consiga()); } } @Override público void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseAccessToken> ficha = cbAccessTokenRepository.findByRefreshToken(extractTokenKey(refreshToken.getValue())); si(ficha.isPresent()){ cbAccessTokenRepository.borrar(ficha.consiga()); } } @Override público OAuth2AccessToken getAccessToken(Autenticación OAuth2 autenticación) { OAuth2AccessToken accessToken = null; Cadena authenticationId = authenticationKeyGenerator.extractKey(autenticación); Opcional<CouchbaseAccessToken> ficha = cbAccessTokenRepository.findByAuthenticationId(authenticationId); si(ficha.isPresent()) { accessToken = ficha.consiga().getToken(); si(accessToken != null && !authenticationId.es igual a(este.authenticationKeyGenerator.extractKey(este.readAuthentication(accessToken)))) { este.removeAccessToken(accessToken); este.storeAccessToken(accessToken, autenticación); } } devolver accessToken; } @Override público Colección<OAuth2AccessToken> findTokensByClientIdAndUserName(Cadena clientId, Cadena nombredeusuario) { Colección<OAuth2AccessToken> fichas = nuevo ArrayList<OAuth2AccessToken>(); Lista<CouchbaseAccessToken> resultado = cbAccessTokenRepository.findByClientIdAndUsername(clientId, nombredeusuario); resultado.paraCada(e-> fichas.añada(e.getToken())); devolver fichas; } @Override público Colección<OAuth2AccessToken> findTokensByClientId(Cadena clientId) { Colección<OAuth2AccessToken> fichas = nuevo ArrayList<OAuth2AccessToken>(); Lista<CouchbaseAccessToken> resultado = cbAccessTokenRepository.findByClientId(clientId); resultado.paraCada(e-> fichas.añada(e.getToken())); devolver fichas; } privado Cadena extractTokenKey(Cadena valor) { si(valor == null) { devolver null; } si no { MessageDigest resumen; pruebe { resumen = MessageDigest.getInstance("MD5"); } captura (NoSuchAlgorithmException var5) { tirar nuevo IllegalStateException("Algoritmo MD5 no disponible. Fatal (debería estar en el JDK)".); } pruebe { byte[] e = resumen.resumen(valor.getBytes("UTF-8")); devolver Cadena.formato("%032x", nuevo Objeto[]{nuevo BigInteger(1, e)}); } captura (UnsupportedEncodingException var4) { tirar nuevo IllegalStateException("Codificación UTF-8 no disponible. Fatal (debería estar en el JDK)".); } } } } |
Por último, podemos modificar ligeramente nuestro SecurityConfig que hemos creado en el artículo anterior. Ahora devolverá una instancia de CouchbaseTokenStore en lugar de InMemoryTokenStore:
1 2 3 4 5 6 7 8 9 10 11 |
@Autowired privado CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired privado CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Judía público TokenStore almacén de fichas() { devolver nuevo CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } |
Aquí está la versión completa del SecurityConfig clase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
@Configuración @EnableWebMvc público clase SecurityConfig extiende WebSecurityConfigurerAdapter { @Autowired privado CustomUserDetailsService customUserDetailsService; @Autowired privado CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired privado CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Autowired público void globalUserDetails(AuthenticationManagerBuilder auth) lanza Excepción { auth.userDetailsService(customUserDetailsService) .codificador de contraseñas(codificador()); } @Override público void configure( WebSecurity web ) lanza Excepción { web.ignorando().antMatchers( HttpMétodo.OPCIONES, "/**" ); } @Override protegido void configure(HttpSeguridad http) lanza Excepción { http .csrf().desactivar() .autorizarSolicitudes() .antMatchers("/oauth/token").permitTodos() .antMatchers("/api-docs/**").permitTodos() .cualquierPetición().autentificado() .y().anónimo().desactivar(); } @Bean público TokenStore almacén de fichas() { devolver nuevo CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } @Bean público Codificador de contraseñas codificador(){ devolver NoOpPasswordEncoder.getInstance(); } @Bean público FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource fuente = nuevo UrlBasedCorsConfigurationSource(); CorsConfiguration config = nuevo CorsConfiguration(); config.setAllowCredentials(verdadero); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); fuente.registerCorsConfiguration("/**", config); FilterRegistrationBean judía = nuevo FilterRegistrationBean(nuevo CorsFilter(fuente)); judía.setOrder(0); devolver judía; } @Bean @Override público AuthenticationManager authenticationManagerBean() lanza Excepción { devolver super.authenticationManagerBean(); } } |
Bien hecho. Eso es todo lo que teníamos que hacer.
Su token de acceso tendrá el siguiente aspecto en su base de datos:
1 2 |
SELECCIONE * de prueba donde Clase = com.bc.quicktask.standalone.model.CouchbaseAccessToken |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[ { "TU_NOMBRE_DEL_CUBO": { "_clase": "com.bc.quicktask.standalone.model.CouchbaseAccessToken", "autentificación": "rO0ABXNyAEFvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5Lm9hdXRoMi5wcm92aWRlci5PQXV0aDJBdXRoZW50aWNhdGlvbr1ACwIWYlITAgACTAANc3RvcmVkUmVxdWVzdHQAPExvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L29hdXRoMi9wcm92aWRlci9PQXV0aDJSZXF1ZXN0O0wAEnVzZXJBdXRoZW50aWNhdGlvbnQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L2NvcmUvQXV0aGVudGljYXRpb247eHIAR29yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkuYXV0aGVudGljYXRpb24uQWJzdHJhY3RBdXRoZW50aWNhdGlvblRva2Vu06oofm5HZA4CAANaAA1hdXRoZW50aWNhdGVkTAALYXV0aG9yaXRpZXN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAHZGV0YWlsc3QAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAHNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWNxAH4ABHhwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+AAxwc3IAOm9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLk9BdXRoMlJlcXVlc3QAAAAAAAAAAQIAB1oACGFwcHJvdmVkTAALYXV0aG9yaXRpZXNxAH4ABEwACmV4dGVuc2lvbnN0AA9MamF2YS91dGlsL01hcDtMAAtyZWRpcmVjdFV, "authenticationId": "202d6940ebd428bbe2098530c8de3958", "clientId": "myclient", "refreshToken": "7613ffc6480ae83beb8f0988ef9ecfcf", "token": { "_clase": "org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", "información adicional": {}, "caducidad": 1537420023432, "refreshToken": { "_clase": "org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", "caducidad": 1537420023421, "valor": "c44db735-403e-49d6-8a22-cfe0e29f21d3" }, "ámbito": [ "confianza", "leer", "escribir" ], "tokenType": "portador", "valor": "7759804c-e1c6-4d63-9520-8737f5b46dbf" }, "tokenId": "12b6bd9de380e1dfab348f4c15abb805", "nombre de usuario": "miusuario" } } ] |
He utilizado de caelwinner proyecto como referencia, he aquí mi especial agradecimiento a él.
Si tiene alguna pregunta, no dude en tuitearme en @deniswsrosa
Hola Denis,
En primer lugar, gracias por el tutorial tan breve pero conciso. Aunque tiene más de un año, está bien explicado, y debo reconocer que eres un excelente profesor.
Por favor, tengo una pregunta y una petición que hacer. Es posible añadir JWT a esta implementación OAuth2, y si sí,¿puede usted por favor proporcionar una guía?
A la espera de una respuesta.
Muchas gracias.