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 |
importação lombok.Dados; importação org.estrutura de mola.dados.anotação.Id; importação org.estrutura de mola.dados.couchbase.núcleo.mapeamento.Documento; importação org.estrutura de mola.segurança.oauth2.comum.OAuth2AccessToken; importação org.estrutura de mola.segurança.oauth2.provedor.Autenticação OAuth2; @Documento @Data público classe CouchbaseAccessToken { @Id privado Cordas id; privado Cordas tokenId; privado OAuth2AccessToken token; privado Cordas authenticationId; privado Cordas nome de usuário; privado Cordas clienteId; privado Cordas autenticação; privado Cordas refreshToken; público Autenticação OAuth2 getAuthentication() { retorno Conversor de objeto serializável.desserializar(autenticação); } público vazio setAuthentication(Autenticação OAuth2 autenticação) { este.autenticação = Conversor de objeto serializável.serializar(autenticação); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "couchbaseAccessToken") público interface CouchbaseAccessTokenRepository se estende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, Cordas> { Lista<CouchbaseAccessToken> findByClientId(Cordas clienteId); Lista<CouchbaseAccessToken> findByClientIdAndUsername(Cordas clienteId, Cordas nome de usuário); Opcional<CouchbaseAccessToken> findByTokenId(Cordas tokenId); Opcional<CouchbaseAccessToken> findByRefreshToken(Cordas refreshToken); Opcional<CouchbaseAccessToken> findByAuthenticationId(Cordas 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 |
importação lombok.Dados; importação org.estrutura de mola.dados.anotação.Id; importação org.estrutura de mola.dados.couchbase.núcleo.mapeamento.Documento; importação org.estrutura de mola.segurança.oauth2.comum.OAuth2RefreshToken; importação org.estrutura de mola.segurança.oauth2.provedor.Autenticação OAuth2; @Documento @Data público classe CouchbaseRefreshToken { @Id privado Cordas id; privado Cordas tokenId; privado OAuth2RefreshToken token; privado Cordas autenticação; público Autenticação OAuth2 getAuthentication() { retorno Conversor de objeto serializável.desserializar(autenticação); } público vazio setAuthentication(Autenticação OAuth2 autenticação) { este.autenticação = Conversor de objeto serializável.serializar(autenticação); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
importação org.estrutura de mola.dados.couchbase.núcleo.consulta.N1qlPrimaryIndexed; importação org.estrutura de mola.dados.couchbase.núcleo.consulta.ViewIndexed; importação org.estrutura de mola.dados.couchbase.repositório.CouchbasePagingAndSortingRepository; importação java.util.Lista; importação java.util.Opcional; @N1qlPrimaryIndexed @ViewIndexed(designDoc = "couchbaseAccessToken") público interface CouchbaseAccessTokenRepository se estende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, Cordas> { Lista<CouchbaseAccessToken> findByClientId(Cordas clienteId); Lista<CouchbaseAccessToken> findByClientIdAndUsername(Cordas clienteId, Cordas nome de usuário); Opcional<CouchbaseAccessToken> findByTokenId(Cordas tokenId); Opcional<CouchbaseAccessToken> findByRefreshToken(Cordas refreshToken); Opcional<CouchbaseAccessToken> findByAuthenticationId(Cordas 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 |
público classe Conversor de objeto serializável { público estático Cordas serializar(Autenticação OAuth2 objeto) { tentar { byte[] bytes = Utilitários de serialização.serializar(objeto); retorno Base64.encodeBase64String(bytes); } captura(Exceção e) { e.printStackTrace(); lançar e; } } público estático Autenticação OAuth2 desserializar(Cordas encodedObject) { tentar { byte[] bytes = Base64.decodificarBase64(encodedObject); retorno (Autenticação OAuth2) Utilitários de serialização.desserializar(bytes); } captura(Exceção e) { e.printStackTrace(); lançar 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 |
importação org.estrutura de mola.segurança.oauth2.comum.OAuth2AccessToken; importação org.estrutura de mola.segurança.oauth2.comum.OAuth2RefreshToken; importação org.estrutura de mola.segurança.oauth2.provedor.Autenticação OAuth2; importação org.estrutura de mola.segurança.oauth2.provedor.token.Gerador de chaves de autenticação; importação org.estrutura de mola.segurança.oauth2.provedor.token.DefaultAuthenticationKeyGenerator (gerador de chave de autenticação padrão); importação org.estrutura de mola.segurança.oauth2.provedor.token.TokenStore; importação java.io.UnsupportedEncodingException; importação java.matemática.BigInteger; importação java.segurança.MessageDigest; importação java.segurança.NoSuchAlgorithmException; importação java.util.*; público classe CouchbaseTokenStore implementa TokenStore { privado CouchbaseAccessTokenRepository cbAccessTokenRepository; privado CouchbaseRefreshTokenRepository cbRefreshTokenRepository; público CouchbaseTokenStore(CouchbaseAccessTokenRepository cbAccessTokenRepository, CouchbaseRefreshTokenRepository cbRefreshTokenRepository){ este.cbAccessTokenRepository = cbAccessTokenRepository; este.cbRefreshTokenRepository = cbRefreshTokenRepository; } privado Gerador de chaves de autenticação gerador de chaves de autenticação = novo DefaultAuthenticationKeyGenerator (gerador de chave de autenticação padrão)(); @Override público Autenticação OAuth2 readAuthentication(OAuth2AccessToken accessToken) { retorno readAuthentication(accessToken.getValue()); } @Override público Autenticação OAuth2 readAuthentication(Cordas token) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(token)); se (accessToken.isPresent()) { retorno accessToken.obter().getAuthentication(); } retorno nulo; } @Override público vazio storeAccessToken(OAuth2AccessToken accessToken, Autenticação OAuth2 autenticação) { Cordas refreshToken = nulo; se (accessToken.getRefreshToken() != nulo) { refreshToken = accessToken.getRefreshToken().getValue(); } se (readAccessToken(accessToken.getValue()) != nulo) { este.removeAccessToken(accessToken); } CouchbaseAccessToken gato = novo CouchbaseAccessToken(); gato.setId(UUID.UUUID aleatório().toString()+UUID.UUUID aleatório().toString()); gato.setTokenId(extractTokenKey(accessToken.getValue())); gato.setToken(accessToken); gato.setAuthenticationId(gerador de chaves de autenticação.extractKey(autenticação)); gato.setUsername(autenticação.isClientOnly() ? nulo : autenticação.getName()); gato.setClientId(autenticação.getOAuth2Request().getClientId()); gato.setAuthentication(autenticação); gato.setRefreshToken(extractTokenKey(refreshToken)); cbAccessTokenRepository.salvar(gato); } @Override público OAuth2AccessToken readAccessToken(Cordas tokenValue) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(tokenValue)); se (accessToken.isPresent()) { retorno accessToken.obter().getToken(); } retorno nulo; } @Override público vazio removeAccessToken(OAuth2AccessToken oAuth2AccessToken) { Opcional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(oAuth2AccessToken.getValue())); se (accessToken.isPresent()) { cbAccessTokenRepository.excluir(accessToken.obter()); } } @Override público vazio storeRefreshToken(OAuth2RefreshToken refreshToken, Autenticação OAuth2 autenticação) { CouchbaseRefreshToken crt = novo CouchbaseRefreshToken(); crt.setId(UUID.UUUID aleatório().toString()+UUID.UUUID aleatório().toString()); crt.setTokenId(extractTokenKey(refreshToken.getValue())); crt.setToken(refreshToken); crt.setAuthentication(autenticação); cbRefreshTokenRepository.salvar(crt); } @Override público OAuth2RefreshToken readRefreshToken(Cordas tokenValue) { Opcional<CouchbaseRefreshToken> refreshToken = cbRefreshTokenRepository.findByTokenId(extractTokenKey(tokenValue)); retorno refreshToken.isPresent()? refreshToken.obter().getToken() :nulo; } @Override público Autenticação OAuth2 readAuthenticationForRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); retorno rtk.isPresent()? rtk.obter().getAuthentication() :nulo; } @Override público vazio removeRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); se (rtk.isPresent()) { cbRefreshTokenRepository.excluir(rtk.obter()); } } @Override público vazio removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { Opcional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByRefreshToken(extractTokenKey(refreshToken.getValue())); se(token.isPresent()){ cbAccessTokenRepository.excluir(token.obter()); } } @Override público OAuth2AccessToken getAccessToken(Autenticação OAuth2 autenticação) { OAuth2AccessToken accessToken = nulo; Cordas authenticationId = gerador de chaves de autenticação.extractKey(autenticação); Opcional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByAuthenticationId(authenticationId); se(token.isPresent()) { accessToken = token.obter().getToken(); se(accessToken != nulo && !authenticationId.iguais(este.gerador de chaves de autenticação.extractKey(este.readAuthentication(accessToken)))) { este.removeAccessToken(accessToken); este.storeAccessToken(accessToken, autenticação); } } retorno accessToken; } @Override público Coleção<OAuth2AccessToken> findTokensByClientIdAndUserName(Cordas clienteId, Cordas nome de usuário) { Coleção<OAuth2AccessToken> tokens = novo ArrayList<OAuth2AccessToken>(); Lista<CouchbaseAccessToken> resultado = cbAccessTokenRepository.findByClientIdAndUsername(clienteId, nome de usuário); resultado.forEach(e-> tokens.adicionar(e.getToken())); retorno tokens; } @Override público Coleção<OAuth2AccessToken> findTokensByClientId(Cordas clienteId) { Coleção<OAuth2AccessToken> tokens = novo ArrayList<OAuth2AccessToken>(); Lista<CouchbaseAccessToken> resultado = cbAccessTokenRepository.findByClientId(clienteId); resultado.forEach(e-> tokens.adicionar(e.getToken())); retorno tokens; } privado Cordas extractTokenKey(Cordas valor) { se(valor == nulo) { retorno nulo; } mais { MessageDigest resumo; tentar { resumo = MessageDigest.getInstance("MD5"); } captura (NoSuchAlgorithmException var5) { lançar novo IllegalStateException("Algoritmo MD5 não disponível. Fatal (deve estar no JDK)."); } tentar { byte[] e = resumo.resumo(valor.getBytes("UTF-8")); retorno Cordas.formato("%032x", novo Objeto[]{novo BigInteger(1, e)}); } captura (UnsupportedEncodingException var4) { lançar novo IllegalStateException("Codificação UTF-8 não disponível. Fatal (deve estar no 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 |
@Com fio automático privado CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Com fio automático privado CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Feijão público TokenStore tokenStore() { retorno novo 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 |
@Configuração @EnableWebMvc público classe SecurityConfig se estende WebSecurityConfigurerAdapter { @Autowired privado CustomUserDetailsService customUserDetailsService; @Autowired privado CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired privado CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Autowired público vazio globalUserDetails(AuthenticationManagerBuilder autenticação) lançamentos Exceção { autenticação.userDetailsService(customUserDetailsService) .passwordEncoder(codificador()); } @Override público vazio configurar( Segurança na Web web ) lançamentos Exceção { web.ignorando().antMatchers( HttpMethod.OPÇÕES, "/**" ); } @Override protegida vazio configurar(HttpSecurity http) lançamentos Exceção { http .csrf().desativar() .authorizeRequests() .antMatchers("/oauth/token").permitAll() .antMatchers("/api-docs/**").permitAll() .anyRequest().autenticado() .e().anônimo().desativar(); } @Bean público TokenStore tokenStore() { retorno novo CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } @Bean público PasswordEncoder codificador(){ retorno NoOpPasswordEncoder.getInstance(); } @Bean público FilterRegistrationBean corsFiltro() { UrlBasedCorsConfigurationSource fonte = novo UrlBasedCorsConfigurationSource(); CorsConfiguration configuração = novo CorsConfiguration(); configuração.setAllowCredentials(verdadeiro); configuração.addAllowedOrigin("*"); configuração.addAllowedHeader("*"); configuração.addAllowedMethod("*"); fonte.registrarCorsConfiguration("/**", configuração); FilterRegistrationBean feijão = novo FilterRegistrationBean(novo CorsFilter(fonte)); feijão.setOrder(0); retorno feijão; } @Bean @Override público Gerenciador de autenticação authenticationManagerBean() lançamentos Exceção { retorno 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 |
SELECIONAR * de teste onde _classe = '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 |
[ { "NOME_DO_SEU_BUCKET": { "_class": "com.bc.quicktask.standalone.model.CouchbaseAccessToken", "authentication" (autenticação): "rO0ABXNyAEFvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5Lm9hdXRoMi5wcm92aWRlci5PQXV0aDJBdXRoZW50aWNhdGlvbr1ACwIWYlITAgACTAANc3RvcmVkUmVxdWVzdHQAPExvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L29hdXRoMi9wcm92aWRlci9PQXV0aDJSZXF1ZXN0O0wAEnVzZXJBdXRoZW50aWNhdGlvbnQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L2NvcmUvQXV0aGVudGljYXRpb247eHIAR29yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkuYXV0aGVudGljYXRpb24uQWJzdHJhY3RBdXRoZW50aWNhdGlvblRva2Vu06oofm5HZA4CAANaAA1hdXRoZW50aWNhdGVkTAALYXV0aG9yaXRpZXN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAHZGV0YWlsc3QAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAHNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWNxAH4ABHhwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+AAxwc3IAOm9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLk9BdXRoMlJlcXVlc3QAAAAAAAAAAQIAB1oACGFwcHJvdmVkTAALYXV0aG9yaXRpZXNxAH4ABEwACmV4dGVuc2lvbnN0AA9MamF2YS91dGlsL01hcDtMAAtyZWRpcmVjdFV, "authenticationId": "202d6940ebd428bbe2098530c8de3958", "clientId": "myclient", "refreshToken": "7613ffc6480ae83beb8f0988ef9ecfcf", "token": { "_class": "org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", "additionalInformation": {}, "expiração": 1537420023432, "refreshToken": { "_class": "org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", "expiração": 1537420023421, "valor": "c44db735-403e-49d6-8a22-cfe0e29f21d3" }, "escopo": [ "confiança", "ler", "escrever" ], "tokenType": "portador", "valor": "7759804c-e1c6-4d63-9520-8737f5b46dbf" }, "tokenId": "12b6bd9de380e1dfab348f4c15abb805", "nome de usuário": "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.