Como você deve ter notado em meus posts anteriores, sou um grande fã de Spring + Java e Primavera + Kotlin. Consequentemente, sempre que preciso implementar uma autenticação OAuth 2.0, a biblioteca spring-security-oauth2 é uma escolha natural.
No entanto, não há quase nada no mercado que mostre como unir o Spring Security e o OAuth2 - conectando o segurança de primavera-oauth2 com diferentes fontes de dados além de inMemory e JDBC. Como temos que configurar muitas coisas, dividirei este tutorial em três partes: Como autenticar um usuário, como configurar um armazenamento de tokens e como configurar clientes dinâmicos. Então, vamos começar!
Primeiro, presumo que você esteja usando uma das versões mais recentes do segurança de primavera-oauth2:
1 2 3 4 5 |
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>segurança de primavera-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> |
Em segundo lugar, estou usando o Couchbase com o Spring Data. Se você estiver usando qualquer outra fonte de dados, ainda poderá reutilizar muitos códigos desta série do blog.
1 2 3 4 5 |
<dependency> <groupId>org.springframework.data</groupId> <artifactId>base de dados de mola</artifactId> <version>3.0.5.RELEASE</version> </dependency> |
Além disso, adicionei Lombok como uma dependência para reduzir o boilerplate do Java:
1 2 3 4 5 |
<dependência> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <opcional>verdadeiro</opcional> </dependência> |
Vamos configurar nosso Resource Server, de acordo com a documentação do Spring Security relacionada a segurança de primavera-oauth2: "Um Servidor de Recursos (pode ser o mesmo que o Servidor de Autorização ou um aplicativo separado) serve recursos que são protegidos pelo token OAuth2. O Spring OAuth fornece um filtro de autenticação do Spring Security que implementa essa proteção. Você pode ativá-lo com @EnableResourceServer em um @Configuração e configurá-lo (conforme necessário) usando um ResourceServerConfigurer"
1 2 3 4 5 6 7 8 9 10 11 |
@Configuração @EnableResourceServer público classe ResourceServerConfig se estende adaptador de servidor de recursos { privado estático final Cordas RESOURCE_ID = "resource_id"; @Override público vazio configurar(ResourceServerSecurityConfigurer recursos) { recursos.resourceId(RESOURCE_ID).sem estado(falso); } } |
Agora, vamos implementar uma interface chamada UserDetailsService. É a interface responsável por fazer a ponte entre sua fonte de dados e o Spring Security OAuth:
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 |
importação com.bc.tarefa rápida.autônomo.modelo.Detalhes do usuário personalizado; importação com.bc.tarefa rápida.autônomo.modelo.Grupo de segurança; importação com.bc.tarefa rápida.autônomo.modelo.Usuário; importação com.bc.tarefa rápida.autônomo.repositório.Repositório de usuários; importação lombok.externo.slf4j.Slf4j; importação org.estrutura de mola.feijões.fábrica.anotação.Com fio automático; importação org.estrutura de mola.segurança.núcleo.detalhes do usuário.UserDetails; importação org.estrutura de mola.segurança.núcleo.detalhes do usuário.UserDetailsService; importação org.estrutura de mola.segurança.núcleo.detalhes do usuário.UsernameNotFoundException; importação org.estrutura de mola.estereótipo.Serviço; importação java.util.Lista; importação java.util.fluxo.Colecionadores; @Slf4j @Serviço público classe CustomUserDetailsService implementa UserDetailsService { @Autowired privado Repositório de usuários userRepository; @Autowired privado SecurityGroupService securityGroupService; @Override público UserDetails loadUserByUsername(Cordas nome) lançamentos UsernameNotFoundException { Lista<User> usuários = userRepository.findByUsername(nome); se(usuários.isEmpty()) { lançar novo UsernameNotFoundException("Não foi possível encontrar o usuário"+nome); } Usuário usuário = usuários.obter(0); Lista<SecurityGroup> securityGroups = securityGroupService.listUserGroups(usuário.getCompanyId(), usuário.getId()); retorno novo Detalhes do usuário personalizado(usuário, securityGroups.fluxo() .mapa(e->e.getId()) .coletar(Colecionadores.toList()) ); } } |
No código acima, estamos retornando uma classe do tipo UserDetailsque também é do Spring. Aqui está sua implementação:
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 |
Dados público classe Detalhes do usuário personalizado implementa UserDetails { privado Usuário usuário; privado Lista<String> grupos; público Detalhes do usuário personalizado(Usuário usuário, Lista<String> grupos) { este.usuário = usuário; este.grupos = grupos; } @Override público Coleção<? se estende GrantedAuthority> getAuthorities() { retorno nulo; } @Override público Cordas getPassword() { retorno usuário.getPassword(); } @Override público Cordas getUsername() { retorno usuário.getUsername(); } @Override público booleano isAccountNonExpired() { retorno verdadeiro; } @Override público booleano isAccountNonLocked() { retorno verdadeiro; } @Override público booleano isCredentialsNonExpired() { retorno verdadeiro; } @Override público booleano isEnabled() { retorno usuário.getIsEnabled(); } } |
Eu poderia ter feito com que a classe User implementasse o UserDetails diretamente. No entanto, como meu caso de uso também exige a lista de grupos em que o usuário está, adicionei a implementação acima.
Veja a seguir a aparência do usuário, do SecurityGroup e de 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 |
@Data público classe Usuário se estende BasicEntity implementa Serializável { @Id @NotNull privado Cordas id; @Campo @NotNull privado Cordas nome de usuário; @Campo @NotNull privado Cordas companyId; @Campo @NotNull privado Cordas senha; @NotNull privado Booleano isEnabled; @Campo privado Booleano isVisible; } |
1 2 3 4 5 6 7 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "usuário") público interface Repositório de usuários se estende CouchbasePagingAndSortingRepository<Usuário, Cordas> { Lista<Usuário> findByUsername(Cordas nome de usuário); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Documento @Data @NoArgsConstructor @Builder público classe Grupo de segurança se estende BasicEntity implementa Serializável { @Id privado Cordas id; @NotNull @Campo privado Cordas nome; @Campo privado Cordas descrição; @NotNull @Campo privado Cordas companyId; @Campo privado Lista<String> usuários = novo ArrayList<>(); @Campo privado booleano removido = falso; } |
1 2 3 4 5 6 7 8 9 10 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "securityGroup" (grupo de segurança)) público interface SecurityGroupRepository se estende CouchbasePagingAndSortingRepository<Grupo de segurança, Cordas> { @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and removed = false" + " AND ARRAY_CONTAINS(users, $2) ") Lista<SecurityGroup> listUserGroups(Cordas companyId, Cordas userId); } |
O BasicEntity A classe também é um pequeno hack para trabalhar melhor com o Spring Data e o Couchbase:
1 2 3 4 5 6 7 |
público classe BasicEntity { @Getter(PROTEGIDO) @Setter(PROTEGIDO) @Ignore protegida Cordas _classe; } |
Por fim, aqui está a implementação da nossa classe SecurityConfig:
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 |
@Configuração @EnableWebMvc público classe SecurityConfig se estende WebSecurityConfigurerAdapter { @Autowired privado CustomUserDetailsService customUserDetailsService; @Bean @Override público Gerenciador de autenticação authenticationManagerBean() lançamentos Exceção { retorno super.authenticationManagerBean(); } @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 Armazenamento de token na memória(); } @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; } } |
Não podemos mais injetar diretamente o bean AuthenticationManager no Spring-Boot 2.0, mas ele ainda é necessário para a estrutura Spring Security. Portanto, precisamos implementar um pequeno hack para obter acesso a esse objeto:
1 2 3 4 5 |
@Bean @Override público Gerenciador de autenticação authenticationManagerBean() lançamentos Exceção { retorno super.authenticationManagerBean(); } |
Vamos dividir essa classe em pequenas partes para entender o que está acontecendo:
1 2 3 4 |
@Bean público PasswordEncoder codificador(){ retorno NoOpPasswordEncoder.getInstance(); } |
A senha do meu usuário está em texto simples, então eu simplesmente retorno uma nova instância de NoOpPasswordEncoder. Um padrão comum é retornar uma instância da classe BCryptPasswordEncoder.
1 2 3 4 |
@Feijão público TokenStore tokenStore() { retorno novo Armazenamento de token na memória(); } |
Por enquanto, usaremos um armazenamento de tokens na memória. Veremos na parte 2 como usar também o Couchbase como um armazenamento de tokens.
1 2 3 4 5 |
@Autowired público vazio globalUserDetails(AuthenticationManagerBuilder autenticação) lançamentos Exceção { autenticação.userDetailsService(customUserDetailsService) .passwordEncoder(codificador()); } |
É aqui que a mágica acontece com o OAuth2 e o Spring Boot. Especificamente, estamos dizendo ao Spring para usar nosso CustomUserDetailsService para pesquisar usuários. Esse bloco de código é a parte principal do que fizemos até agora.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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; } |
Esse bloco nos permitirá fazer solicitações usando CORS (Cross-Origin Resource Sharing)
1 2 3 4 |
@Override público vazio configurar( Segurança na Web web ) lançamentos Exceção { web.ignorando().antMatchers( HttpMethod.OPÇÕES, "/**" ); } |
E, por fim, se você precisar chamar sua API por meio do JQuery, também precisará adicionar o código acima. Caso contrário, você receberá uma mensagem "A resposta para o preflight não tem o status HTTP ok." Erro.
Agora só falta uma coisa: precisamos adicionar um servidor de autorização:
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 |
importação org.estrutura de mola.feijões.fábrica.anotação.Com fio automático; importação org.estrutura de mola.contexto.anotação.Configuração; importação org.estrutura de mola.segurança.autenticação.Gerenciador de autenticação; importação org.estrutura de mola.segurança.oauth2.configuração.anotação.configuradores.ClientDetailsServiceConfigurer; importação org.estrutura de mola.segurança.oauth2.configuração.anotação.web.configuração.AuthorizationServerConfigurerAdapter; importação org.estrutura de mola.segurança.oauth2.configuração.anotação.web.configuração.EnableAuthorizationServer (Ativar servidor de autorização); importação org.estrutura de mola.segurança.oauth2.configuração.anotação.web.configuradores.AuthorizationServerEndpointsConfigurer; importação org.estrutura de mola.segurança.oauth2.provedor.token.TokenStore; @Configuração @EnableAuthorizationServer público classe AuthorizationServerConfig se estende AuthorizationServerConfigurerAdapter { estático final Cordas ID_CLIENTE = "android-client"; estático final Cordas SEGREDO DO CLIENTE = "android-secret"; estático final Cordas GRANT_TYPE_PASSWORD = "senha"; estático final Cordas AUTHORIZATION_CODE = "authorization_code"; estático final Cordas REFRESH_TOKEN = "refresh_token"; estático final Cordas IMPLÍCITO = "implícito"; estático final Cordas SCOPE_READ = "ler"; estático final Cordas SCOPE_WRITE = "escrever"; estático final Cordas CONFIANÇA = "confiança"; estático final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60; estático final int REFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; @Autowired privado TokenStore tokenStore; @Autowired privado Gerenciador de autenticação gerenciador de autenticação; @Override público vazio configurar(ClientDetailsServiceConfigurer configurador) lançamentos Exceção { configurador .inMemory() .comCliente(ID_CLIENTE) .segredo(SEGREDO DO CLIENTE) .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLÍCITO ) .escopos(SCOPE_READ, SCOPE_WRITE, CONFIANÇA) .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS); } @Override público vazio configurar(AuthorizationServerEndpointsConfigurer pontos finais) lançamentos Exceção { pontos finais.tokenStore(tokenStore) .gerenciador de autenticação(gerenciador de autenticação); } @Override público vazio configurar(AuthorizationServerSecurityConfigurer oauthServer) lançamentos Exceção { oauthServer.tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } } |
Muito bem, agora você pode iniciar seu aplicativo e chamá-lo por meio do Postman ou do Jquery:
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 |
var dados = { "grant_type": "senha", "nome de usuário": "myuser", "senha":"mypassword" (minha senha), "client_id":"android-client", "client_secret":"android-secret" } $.ajax({ 'url': "http://localhost:8080/oauth/token", 'tipo': 'POST', "crossDomain" (domínio cruzado): verdadeiro, "cabeçalhos": { 'Autorização': 'Basic YW5kcm9pZC1jbGllbnQ6YW5kcm9pZC1zZWNyZXQ=', //android-client:android-secret em Base64 'Content-Type':'application/x-www-form-urlencoded'}, "dados":dados, 'sucesso': função (resultado) { console.registro( "Meu token de acesso = "+ resultado.token_de_acesso); console.registro( "Meu token de atualização = "+ resultado.refresh_token); console.registro("expira em = "+resultado.expira_em) succesCallback() }, 'erro': função (XMLHttpRequest, textStatus, errorThrown) { errorCallback(XMLHttpRequest, textStatus, errorThrown) } }); |
Aumento do desempenho
Se você estiver usando o Couchbase, sugiro que use o nome de usuário como a chave do seu documento. Isso permitirá que você use o Armazenamento de chave-valor em vez de executar consultas N1QL, o que aumentará significativamente o desempenho de seu login.
Se você tiver alguma dúvida sobre o Couchbase, a segurança do OAuth ou a otimização da autenticação do Spring e do OAuth2, envie um tweet para @deniswsrosa
Artigo muito bom. Você poderia compartilhar o código desta postagem?