Na postagem anterior do blog, discutimos Como configurar uma autenticação OAuth2 simples. No entanto, nossa implementação tem uma falha importante: estamos usando um armazenamento de tokens na memória.
Os armazenamentos de tokens na memória devem ser usados somente durante o desenvolvimento ou se o seu aplicativo tiver um único servidor, pois não é possível compartilhá-los facilmente entre os nós e, no caso de reinicialização do servidor, você perderá todos os tokens de acesso nele contidos.
Spring-security-oauth2 já tem suporte integrado para JDBC e JWT. No entanto, se você precisar salvar seus tokens em outro lugar, terá de criar seu próprio armazenamento de tokens de segurança do Spring. Infelizmente, implementar tal coisa não é uma tarefa trivial, e espero que a receita a seguir lhe poupe algumas horas de trabalho.
Vamos começar criando as duas entidades responsáveis por armazenar seu token de acesso e de atualização e seus respectivos repositórios:
|
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 |
import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; @Document @Data public class CouchbaseAccessToken { @Id private String id; private String tokenId; private OAuth2AccessToken token; private String authenticationId; private String username; private String clientId; private String authentication; private String refreshToken; public OAuth2Authentication getAuthentication() { return SerializableObjectConverter.deserialize(authentication); } public void setAuthentication(OAuth2Authentication authentication) { this.authentication = SerializableObjectConverter.serialize(authentication); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "couchbaseAccessToken") public interface CouchbaseAccessTokenRepository extends CouchbasePagingAndSortingRepository<CouchbaseAccessToken, String> { List<CouchbaseAccessToken> findByClientId(String clientId); List<CouchbaseAccessToken> findByClientIdAndUsername(String clientId, String username); Optional<CouchbaseAccessToken> findByTokenId(String tokenId); Optional<CouchbaseAccessToken> findByRefreshToken(String refreshToken); Optional<CouchbaseAccessToken> findByAuthenticationId(String 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 |
import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; @Document @Data public class CouchbaseRefreshToken { @Id private String id; private String tokenId; private OAuth2RefreshToken token; private String authentication; public OAuth2Authentication getAuthentication() { return SerializableObjectConverter.deserialize(authentication); } public void setAuthentication(OAuth2Authentication authentication) { this.authentication = SerializableObjectConverter.serialize(authentication); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; import org.springframework.data.couchbase.core.query.ViewIndexed; import org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository; import java.util.List; import java.util.Optional; @N1qlPrimaryIndexed @ViewIndexed(designDoc = "couchbaseAccessToken") public interface CouchbaseAccessTokenRepository extends CouchbasePagingAndSortingRepository<CouchbaseAccessToken, String> { List<CouchbaseAccessToken> findByClientId(String clientId); List<CouchbaseAccessToken> findByClientIdAndUsername(String clientId, String username); Optional<CouchbaseAccessToken> findByTokenId(String tokenId); Optional<CouchbaseAccessToken> findByRefreshToken(String refreshToken); Optional<CouchbaseAccessToken> findByAuthenticationId(String authenticationId); } |
Observe que Autenticação OAuth2 é uma interface, portanto, não tenho outra opção a não ser serializar o objeto para armazená-lo no banco de dados. Aqui está a classe responsável por serializar/desserializar o objeto:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class SerializableObjectConverter { public static String serialize(OAuth2Authentication object) { try { byte[] bytes = SerializationUtils.serialize(object); return Base64.encodeBase64String(bytes); } catch(Exception e) { e.printStackTrace(); throw e; } } public static OAuth2Authentication deserialize(String encodedObject) { try { byte[] bytes = Base64.decodeBase64(encodedObject); return (OAuth2Authentication) SerializationUtils.deserialize(bytes); } catch(Exception e) { e.printStackTrace(); throw e; } } |
Agora, podemos finalmente criar nosso spring oauth2 armazenamento de tokens. Para fazer isso, tudo o que precisamos é implementar a longa lista de métodos da classe 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 |
import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator; import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; import org.springframework.security.oauth2.provider.token.TokenStore; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; public class CouchbaseTokenStore implements TokenStore { private CouchbaseAccessTokenRepository cbAccessTokenRepository; private CouchbaseRefreshTokenRepository cbRefreshTokenRepository; public CouchbaseTokenStore(CouchbaseAccessTokenRepository cbAccessTokenRepository, CouchbaseRefreshTokenRepository cbRefreshTokenRepository){ this.cbAccessTokenRepository = cbAccessTokenRepository; this.cbRefreshTokenRepository = cbRefreshTokenRepository; } private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); @Override public OAuth2Authentication readAuthentication(OAuth2AccessToken accessToken) { return readAuthentication(accessToken.getValue()); } @Override public OAuth2Authentication readAuthentication(String token) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(token)); if (accessToken.isPresent()) { return accessToken.get().getAuthentication(); } return null; } @Override public void storeAccessToken(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { String refreshToken = null; if (accessToken.getRefreshToken() != null) { refreshToken = accessToken.getRefreshToken().getValue(); } if (readAccessToken(accessToken.getValue()) != null) { this.removeAccessToken(accessToken); } CouchbaseAccessToken cat = new CouchbaseAccessToken(); cat.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); cat.setTokenId(extractTokenKey(accessToken.getValue())); cat.setToken(accessToken); cat.setAuthenticationId(authenticationKeyGenerator.extractKey(authentication)); cat.setUsername(authentication.isClientOnly() ? null : authentication.getName()); cat.setClientId(authentication.getOAuth2Request().getClientId()); cat.setAuthentication(authentication); cat.setRefreshToken(extractTokenKey(refreshToken)); cbAccessTokenRepository.save(cat); } @Override public OAuth2AccessToken readAccessToken(String tokenValue) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(tokenValue)); if (accessToken.isPresent()) { return accessToken.get().getToken(); } return null; } @Override public void removeAccessToken(OAuth2AccessToken oAuth2AccessToken) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(oAuth2AccessToken.getValue())); if (accessToken.isPresent()) { cbAccessTokenRepository.delete(accessToken.get()); } } @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { CouchbaseRefreshToken crt = new CouchbaseRefreshToken(); crt.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); crt.setTokenId(extractTokenKey(refreshToken.getValue())); crt.setToken(refreshToken); crt.setAuthentication(authentication); cbRefreshTokenRepository.save(crt); } @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) { Optional<CouchbaseRefreshToken> refreshToken = cbRefreshTokenRepository.findByTokenId(extractTokenKey(tokenValue)); return refreshToken.isPresent()? refreshToken.get().getToken() :null; } @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); return rtk.isPresent()? rtk.get().getAuthentication() :null; } @Override public void removeRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); if (rtk.isPresent()) { cbRefreshTokenRepository.delete(rtk.get()); } } @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByRefreshToken(extractTokenKey(refreshToken.getValue())); if(token.isPresent()){ cbAccessTokenRepository.delete(token.get()); } } @Override public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { OAuth2AccessToken accessToken = null; String authenticationId = authenticationKeyGenerator.extractKey(authentication); Optional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByAuthenticationId(authenticationId); if(token.isPresent()) { accessToken = token.get().getToken(); if(accessToken != null && !authenticationId.equals(this.authenticationKeyGenerator.extractKey(this.readAuthentication(accessToken)))) { this.removeAccessToken(accessToken); this.storeAccessToken(accessToken, authentication); } } return accessToken; } @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { Collection<OAuth2AccessToken> tokens = new ArrayList<OAuth2AccessToken>(); List<CouchbaseAccessToken> result = cbAccessTokenRepository.findByClientIdAndUsername(clientId, userName); result.forEach(e-> tokens.add(e.getToken())); return tokens; } @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { Collection<OAuth2AccessToken> tokens = new ArrayList<OAuth2AccessToken>(); List<CouchbaseAccessToken> result = cbAccessTokenRepository.findByClientId(clientId); result.forEach(e-> tokens.add(e.getToken())); return tokens; } private String extractTokenKey(String value) { if(value == null) { return null; } else { MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var5) { throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK)."); } try { byte[] e = digest.digest(value.getBytes("UTF-8")); return String.format("%032x", new Object[]{new BigInteger(1, e)}); } catch (UnsupportedEncodingException var4) { throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK)."); } } } } |
Por fim, podemos alterar ligeiramente nosso SecurityConfig que criamos no artigo anterior. Ele retornará agora uma instância de CouchbaseTokenStore em vez de Armazenamento de token na memória:
|
1 2 3 4 5 6 7 8 9 10 11 |
@Autowired private CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired private CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Bean public TokenStore tokenStore() { return new CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } |
Aqui está a versão completa do SecurityConfig classe:
|
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 |
@Configuration @EnableWebMvc public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired private CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService) .passwordEncoder(encoder()); } @Override public void configure( WebSecurity web ) throws Exception { web.ignoring().antMatchers( HttpMethod.OPTIONS, "/**" ); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/oauth/token").permitAll() .antMatchers("/api-docs/**").permitAll() .anyRequest().authenticated() .and().anonymous().disable(); } @Bean public TokenStore tokenStore() { return new CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } @Bean public PasswordEncoder encoder(){ return NoOpPasswordEncoder.getInstance(); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } |
Muito bem! Isso é tudo o que tínhamos que fazer.
Seu token de acesso será parecido com o seguinte em seu banco de dados:
|
1 2 |
SELECT * from test where _class = '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 |
[ { "YOUR_BUCKET_NAME": { "_class": "com.bc.quicktask.standalone.model.CouchbaseAccessToken", "authentication": "rO0ABXNyAEFvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5Lm9hdXRoMi5wcm92aWRlci5PQXV0aDJBdXRoZW50aWNhdGlvbr1ACwIWYlITAgACTAANc3RvcmVkUmVxdWVzdHQAPExvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L29hdXRoMi9wcm92aWRlci9PQXV0aDJSZXF1ZXN0O0wAEnVzZXJBdXRoZW50aWNhdGlvbnQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L2NvcmUvQXV0aGVudGljYXRpb247eHIAR29yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkuYXV0aGVudGljYXRpb24uQWJzdHJhY3RBdXRoZW50aWNhdGlvblRva2Vu06oofm5HZA4CAANaAA1hdXRoZW50aWNhdGVkTAALYXV0aG9yaXRpZXN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAHZGV0YWlsc3QAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAHNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWNxAH4ABHhwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+AAxwc3IAOm9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLk9BdXRoMlJlcXVlc3QAAAAAAAAAAQIAB1oACGFwcHJvdmVkTAALYXV0aG9yaXRpZXNxAH4ABEwACmV4dGVuc2lvbnN0AA9MamF2YS91dGlsL01hcDtMAAtyZWRpcmVjdFVyaXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAB3JlZnJlc2h0ADtMb3JnL3NwcmluZ2ZyYW1ld29yay9zZWN1cml0eS9vYXV0aDIvcHJvdmlkZXIvVG9rZW5SZXF1ZXN0O0wAC3Jlc291cmNlSWRzdAAPTGphdmEvdXRpbC9TZXQ7TAANcmVzcG9uc2VUeXBlc3EAfgAReHIAOG9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLkJhc2VSZXF1ZXN0Nih6PqNxab0CAANMAAhjbGllbnRJZHEAfgAPTAARcmVxdWVzdFBhcmFtZXRlcnNxAH4ADkwABXNjb3BlcQB+ABF4cHQACG15Y2xpZW50c3IAJWphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVNYXDxpaj+dPUHQgIAAUwAAW1xAH4ADnhwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAABncIAAAACAAAAAN0AApncmFudF90eXBldAAIcGFzc3dvcmR0AAljbGllbnRfaWR0AAhteWNsaWVudHQACHVzZXJuYW1ldAAGbXl1c2VyeHNyACVqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlU2V0gB2S0Y+bgFUCAAB4cQB+AAlzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hTZXTYbNdald0qHgIAAHhyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAABA/QAAAAAAAA3QABXRydXN0dAAEcmVhZHQABXdyaXRleAFzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcQB+ABc/QAAAAAAAAHcIAAAAEAAAAAB4cHBzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcgBPb3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eS5hdXRoZW50aWNhdGlvbi5Vc2VybmFtZVBhc3N3b3JkQXV0aGVudGljYXRpb25Ub2tlbgAAAAAAAAH0AgACTAALY3JlZGVudGlhbHNxAH4ABUwACXByaW5jaXBhbHEAfgAFeHEAfgADAXNyAB9qYXZhLnV0aWwuQ29sbGVjdGlvbnMkRW1wdHlMaXN0ergXtDynnt4CAAB4cHNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cQB+ABc/QAAAAAAABncIAAAACAAAAAR0AA1jbGllbnRfc2VjcmV0dAAIbXlzZWNyZXRxAH4AGXEAfgAacQB+ABtxAH4AHHEAfgAdcQB+AB54AHBzcgAyY29tLmJjLnF1aWNrdGFzay5zdGFuZGFsb25lLm1vZGVsLkN1c3RvbVVzZXJEZXRhaWz9dbY7wdosOwIAAkwABmdyb3Vwc3EAfgAITAAEdXNlcnQAKExjb20vYmMvcXVpY2t0YXNrL3N0YW5kYWxvbmUvbW9kZWwvVXNlcjt4cHNxAH4ACwAAAAB3BAAAAAB4c3IAJmNvbS5iYy5xdWlja3Rhc2suc3RhbmRhbG9uZS5tb2RlbC5Vc2VyWvIkR494dqQCAAdMAAljb21wYW55SWRxAH4AD0wADWV4dGVybmFsTG9naW5xAH4AD0wAAmlkcQB+AA9MAAlpc0VuYWJsZWR0ABNMamF2YS9sYW5nL0Jvb2xlYW47TAAJaXNWaXNpYmxlcQB+ADhMAAhwYXNzd29yZHEAfgAPTAAIdXNlcm5hbWVxAH4AD3hwdAAKY29tcGFueS0tMXQABm15dXNlcnQACXVzZXJJZC0tMXNyABFqYXZhLmxhbmcuQm9vbGVhbs0gcoDVnPruAgABWgAFdmFsdWV4cAFxAH4APnQACHBhc3N3b3JkdAAGbXl1c2Vy", "authenticationId": "202d6940ebd428bbe2098530c8de3958", "clientId": "myclient", "refreshToken": "7613ffc6480ae83beb8f0988ef9ecfcf", "token": { "_class": "org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", "additionalInformation": {}, "expiration": 1537420023432, "refreshToken": { "_class": "org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", "expiration": 1537420023421, "value": "c44db735-403e-49d6-8a22-cfe0e29f21d3" }, "scope": [ "trust", "read", "write" ], "tokenType": "bearer", "value": "7759804c-e1c6-4d63-9520-8737f5b46dbf" }, "tokenId": "12b6bd9de380e1dfab348f4c15abb805", "username": "myuser" } } ] |
Eu usei caelwinner's como referência, aqui está meu agradecimento especial a ele.
Se você tiver alguma dúvida, sinta-se à vontade para me enviar um tweet para @deniswsrosa
Oi Denis,
Antes de mais nada, obrigado pelo tutorial muito breve, mas conciso. Embora tenha mais de um ano, ele está bem explicado, e devo reconhecer que você é um excelente professor.
Por favor, tenho uma pergunta e uma solicitação a fazer. É possível adicionar o JWT a essa implementação OAuth2 e, em caso afirmativo, você pode fornecer um guia?
Aguardando uma resposta.
Muito obrigado.