portaldacalheta.pt
  • Principal
  • Receita E Crescimento
  • Aprendendo
  • Processo Interno
  • Processo De Design
Processo Interno

Usando Spring Boot para proteção OAuth2 e JWT REST



Este artigo é um guia sobre como configurar uma implementação do lado do servidor de JSON Web Token (JWT) - OAuth2 estrutura de autorização usando Spring Boot e Maven .

Uma compreensão inicial do OAuth2 é recomendada e pode ser obtida lendo o rascunho no link acima ou pesquisando informações úteis na web, como isto ou isto .



OAuth2 é uma estrutura de autorização que substitui a primeira versão OAuth, criada em 2006. Ele define os fluxos de autorização entre clientes e um ou mais serviços HTTP para obter acesso a recursos protegidos.



OAuth2 define as seguintes funções do lado do servidor:



  • Proprietário do recurso: O serviço responsável por controlar o acesso aos recursos
  • Servidor de recursos: O serviço que realmente fornece os recursos
  • Servidor de autorização: O processo de autorização de tratamento de serviço atuando como um intermediário entre o cliente e o proprietário do recurso

JSON Web Token, ou JWT, é uma especificação para a representação de reivindicações a serem transferidas entre duas partes. As declarações são codificadas como um objeto JSON usado como carga útil de uma estrutura criptografada, permitindo que as declarações sejam assinadas ou criptografadas digitalmente.

A estrutura que contém pode ser JSON Web Signature (JWS) ou JSON Web Encryption (JWE).



O JWT pode ser escolhido como o formato para tokens de acesso e atualização usados ​​dentro do protocolo OAuth2.

O OAuth2 e o JWT ganharam grande popularidade nos últimos anos devido aos seguintes recursos:



  • Fornece um sistema de autorização sem estado para protocolo REST sem estado
  • Se encaixa bem em uma arquitetura de micro-serviço em que vários servidores de recursos podem compartilhar um único servidor de autorização
  • Conteúdo de token fácil de gerenciar no lado do cliente devido ao formato JSON

No entanto, OAuth2 e JWT nem sempre são a melhor escolha caso as seguintes considerações sejam importantes para o projeto:

  • Um protocolo sem estado não permite a revogação de acesso no lado do servidor
  • Vida útil fixa para token adiciona complexidade adicional para o gerenciamento de sessões de longa duração sem comprometer a segurança (por exemplo, token de atualização)
  • Um requisito para um armazenamento seguro de um token no lado do cliente

Fluxo de protocolo esperado

Embora um dos principais recursos do OAuth2 seja a introdução de uma camada de autorização para separar o processo de autorização dos proprietários de recursos, por uma questão de simplicidade, o resultado do artigo é a construção de um único aplicativo que representa todos dono do recurso , servidor de autorização e servidor de recursos papéis. Por causa disso, a comunicação fluirá entre apenas duas entidades, o servidor e o cliente.



pontos de interrupção padrão de consultas de mídia css

Esta simplificação deve ajudar a focar no objetivo do artigo, ou seja, a configuração de tal sistema em um ambiente Spring Boot.

O fluxo simplificado é descrito abaixo:



  1. O pedido de autorização é enviado do cliente para o servidor (agindo como proprietário do recurso) usando concessão de autorização de senha
  2. Token de acesso é devolvido ao cliente (junto com token de atualização )
  3. O token de acesso é então enviado do cliente para o servidor (atuando como servidor de recursos) em cada solicitação de acesso a recursos protegidos
  4. O servidor responde com os recursos protegidos necessários

Diagrama de fluxo de autenticação

Spring Security e Spring Boot

Em primeiro lugar, uma breve introdução à pilha de tecnologia selecionada para este projeto.



c-corp vs s-corp

A ferramenta de gerenciamento de projeto de escolha é Maven , mas devido à simplicidade do projeto, não deve ser difícil mudar para outras ferramentas como Gradle .

Na continuação do artigo, enfocamos apenas os aspectos do Spring Security, mas todos os trechos de código são retirados de um aplicativo do lado do servidor totalmente funcional, cujo código-fonte está disponível em um repositório público junto com um cliente consumindo seus recursos REST.

Spring Security é uma estrutura que fornece serviços de segurança quase declarativos para aplicativos baseados em Spring. Suas raízes vêm do início do Spring e é organizado como um conjunto de módulos devido ao grande número de diferentes tecnologias de segurança cobertas.

Vamos dar uma olhada rápida na arquitetura Spring Security (um guia mais detalhado pode ser encontrado Aqui )

A segurança é principalmente sobre autenticação , ou seja, a verificação da identidade, e autorização , a concessão de direitos de acesso a recursos.

A segurança Spring oferece suporte a uma grande variedade de modelos de autenticação, fornecidos por terceiros ou implementados nativamente. Uma lista pode ser encontrada Aqui .

Em relação à autorização, três áreas principais são identificadas:

  1. Web solicita autorização
  2. Autorização de nível de método
  3. Acesso à autorização de instâncias de objeto de domínio

Autenticação

A interface básica é AuthenticationManager que é responsável por fornecer um método de autenticação. O UserDetailsService é a interface relacionada à coleta de informações do usuário, que pode ser implementada diretamente ou usada internamente no caso de métodos JDBC ou LDAP padrão.

Autorização

A interface principal é AccessDecisionManager; cujas implementações para todas as três áreas listadas acima delegam a uma cadeia de AccessDecisionVoter. Cada instância da última interface representa uma associação entre um Authentication (uma identidade de usuário, principal nomeada), um recurso e uma coleção de ConfigAttribute, o conjunto de regras que descreve como o proprietário do recurso permitiu o acesso ao próprio recurso, talvez através do uso de funções de usuário.

A segurança de um aplicativo da web é implementada usando os elementos básicos descritos acima em uma cadeia de filtros de servlet e a classe WebSecurityConfigurerAdapter é exposto como uma forma declarativa de expressar as regras de acesso do recurso.

javascript o que é um nó

A segurança do método é primeiro habilitada pela presença de @EnableGlobalMethodSecurity(securedEnabled = true) anotação e, em seguida, pelo uso de um conjunto de anotações especializadas para aplicar a cada método a ser protegido, como @Secured, @PreAuthorize e @PostAuthorize.

O Spring Boot adiciona a tudo isso uma coleção de configurações de aplicativos opinativos e bibliotecas de terceiros para facilitar o desenvolvimento enquanto mantém um padrão de alta qualidade.

JWT OAuth2 com Spring Boot

Agora vamos passar ao problema original para configurar um aplicativo que implementa OAuth2 e JWT com Spring Boot.

Embora existam várias bibliotecas OAuth2 do lado do servidor no mundo Java (uma lista pode ser encontrada Aqui ), a implementação baseada em spring é a escolha natural, pois esperamos encontrá-la bem integrada na arquitetura Spring Security e, portanto, evitar a necessidade de lidar com muitos dos detalhes de baixo nível para seu uso.

Todas as dependências da biblioteca relacionadas à segurança são tratadas pelo Maven com a ajuda do Spring Boot, que é o único componente que requer uma versão explícita dentro do arquivo de configuração do maven pom.xml (ou seja, as versões da biblioteca são inferidas automaticamente pelo Maven escolhendo a versão mais atualizada compatível com a versão do Spring Boot inserida).

Encontre abaixo o trecho do arquivo de configuração do maven pom.xml contendo as dependências relacionadas à segurança do Spring Boot:

org.springframework.boot spring-boot-starter-security org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.0.RELEASE

O aplicativo atua como servidor de autorização / proprietário de recurso OAuth2 e como servidor de recurso.

Os recursos protegidos (como servidor de recursos) são publicados em /fogo/ caminho, enquanto o caminho de autenticação (como proprietário do recurso / servidor de autorização) é mapeado para / oauth / token , seguindo o padrão proposto.

Estrutura do aplicativo:

  • security pacote contendo configuração de segurança
  • errors pacote contendo tratamento de erros
  • users, glee pacotes para recursos REST, incluindo modelo, repositório e controlador

Os próximos parágrafos cobrem a configuração de cada uma das três funções OAuth2 mencionadas acima. As classes relacionadas estão dentro de security pacote:

  • OAuthConfiguration, estendendo AuthorizationServerConfigurerAdapter
  • ResourceServerConfiguration, estendendo ResourceServerConfigurerAdapter
  • ServerSecurityConfig, estendendo WebSecurityConfigurerAdapter
  • UserService, implementando UserDetailsService

Configuração para proprietário de recurso e servidor de autorização

O comportamento do servidor de autorização é ativado pela presença de @EnableAuthorizationServer anotação. Sua configuração é mesclada com aquela relacionada ao comportamento do proprietário do recurso e ambas estão contidas na classe AuthorizationServerConfigurerAdapter.

tutorial de dinâmica de corpos rígidos

As configurações aplicadas aqui estão relacionadas a:

  • Acesso do cliente (usando ClientDetailsServiceConfigurer)
    • Seleção de usar um armazenamento em memória ou baseado em JDBC para detalhes do cliente com inMemory ou jdbc métodos
    • Autenticação básica do cliente usando clientId e clientSecret (codificado com os atributos PasswordEncoder bean escolhidos)
    • Tempo de validade para acessar e atualizar tokens usando accessTokenValiditySeconds e refreshTokenValiditySeconds atributos
    • Tipos de concessão permitidos usando authorizedGrantTypes atributo
    • Define escopos de acesso com scopes método
    • Identifique os recursos acessíveis do cliente
  • Endpoint do servidor de autorização (usando AuthorizationServerEndpointsConfigurer)
    • Defina o uso de um token JWT com accessTokenConverter
    • Defina o uso de um UserDetailsService e AuthenticationManager interfaces para realizar autenticação (como proprietário do recurso)
package net.reliqs.gleeometer.security; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; @Configuration @EnableAuthorizationServer public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; private final UserDetailsService userService; @Value('${jwt.clientId:glee-o-meter}') private String clientId; @Value('${jwt.client-secret:secret}') private String clientSecret; @Value('${jwt.signing-key:123}') private String jwtSigningKey; @Value('${jwt.accessTokenValidititySeconds:43200}') // 12 hours private int accessTokenValiditySeconds; @Value('${jwt.authorizedGrantTypes:password,authorization_code,refresh_token}') private String[] authorizedGrantTypes; @Value('${jwt.refreshTokenValiditySeconds:2592000}') // 30 days private int refreshTokenValiditySeconds; public OAuthConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.userService = userService; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(clientId) .secret(passwordEncoder.encode(clientSecret)) .accessTokenValiditySeconds(accessTokenValiditySeconds) .refreshTokenValiditySeconds(refreshTokenValiditySeconds) .authorizedGrantTypes(authorizedGrantTypes) .scopes('read', 'write') .resourceIds('api'); } @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) { endpoints .accessTokenConverter(accessTokenConverter()) .userDetailsService(userService) .authenticationManager(authenticationManager); } @Bean JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); return converter; } }

A próxima seção descreve a configuração a ser aplicada ao servidor de recursos.

Configuração para servidor de recursos

O comportamento do servidor de recursos é ativado pelo uso de @EnableResourceServer a anotação e sua configuração estão contidas na classe ResourceServerConfiguration.

A única configuração necessária aqui é a definição da identificação do recurso para coincidir com o acesso do cliente definido na aula anterior.

package net.reliqs.gleeometer.security; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId('api'); } }

O último elemento de configuração é sobre a definição da segurança do aplicativo da web.

Configuração de segurança da web

A configuração de segurança da Web do Spring está contida na classe ServerSecurityConfig, ativada pelo uso de @EnableWebSecurity anotação. O @EnableGlobalMethodSecurity permite especificar a segurança no nível do método. Seu atributo proxyTargetClass é definido para que funcione para os métodos de RestController, porque os controladores geralmente são classes, não implementando nenhuma interface.

Ele define o seguinte:

  • O provedor de autenticação a ser usado, definindo o bean authenticationProvider
  • O codificador de senha a ser usado, definindo o bean passwordEncoder
  • O bean do gerenciador de autenticação
  • A configuração de segurança para os caminhos publicados usando HttpSecurity
  • Uso de um personalizado AuthenticationEntryPoint para lidar com mensagens de erro fora do manipulador de erros Spring REST padrão ResponseEntityExceptionHandler
package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler; import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final UserDetailsService userDetailsService; public ServerSecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, @Qualifier('userService') UserDetailsService userDetailsService) { this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; this.userDetailsService = userDetailsService; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(userDetailsService); return provider; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers('/api/signin/**').permitAll() .antMatchers('/api/glee/**').hasAnyAuthority('ADMIN', 'USER') .antMatchers('/api/users/**').hasAuthority('ADMIN') .antMatchers('/api/**').authenticated() .anyRequest().authenticated() .and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler()); } }

O extrato de código abaixo é sobre a implementação de UserDetailsService interface para fornecer a autenticação do proprietário do recurso.

package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.users.User; import net.reliqs.gleeometer.users.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class UserService implements UserDetailsService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException('User not found: ' + username)); GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name()); return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority)); } }

A próxima seção é sobre a descrição de uma implementação do controlador REST para ver como as restrições de segurança são mapeadas.

Controlador REST

Dentro do controlador REST, podemos encontrar duas maneiras de aplicar o controle de acesso para cada método de recurso:

  • Usando uma instância de OAuth2Authentication passado pelo Spring como um parâmetro
  • Usando @PreAuthorize ou @PostAuthorize anotações
package net.reliqs.gleeometer.users; import lombok.extern.slf4j.Slf4j; import net.reliqs.gleeometer.errors.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.Size; import java.util.HashSet; @RestController @RequestMapping('/api/users') @Slf4j @Validated class UserController { private final UserRepository repository; private final PasswordEncoder passwordEncoder; UserController(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; this.passwordEncoder = passwordEncoder; } @GetMapping Page all(@PageableDefault(size = Integer.MAX_VALUE) Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmail(auth, pageable); } return repository.findAll(pageable); } @GetMapping('/search') Page search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmailContainsAndEmail(email, auth, pageable); } return repository.findByEmailContains(email, pageable); } @GetMapping('/findByEmail') @PreAuthorize('!hasAuthority('USER') || (authentication.principal == #email)') User findByEmail(@RequestParam String email, OAuth2Authentication authentication) { return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, 'email', email)); } @GetMapping('/{id}') @PostAuthorize('!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)') User one(@PathVariable Long id) { return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); } @PutMapping('/{id}') @PreAuthorize('!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)') void update(@PathVariable Long id, @Valid @RequestBody User res) { User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); res.setPassword(u.getPassword()); res.setGlee(u.getGlee()); repository.save(res); } @PostMapping @PreAuthorize('!hasAuthority('USER')') User create(@Valid @RequestBody User res) { return repository.save(res); } @DeleteMapping('/{id}') @PreAuthorize('!hasAuthority('USER')') void delete(@PathVariable Long id) { if (repository.existsById(id)) { repository.deleteById(id); } else { throw new EntityNotFoundException(User.class, 'id', id.toString()); } } @PutMapping('/{id}/changePassword') @PreAuthorize('!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)') void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) { User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) { user.setPassword(passwordEncoder.encode(newPassword)); repository.save(user); } else { throw new ConstraintViolationException('old password doesn't match', new HashSet()); } } }

Conclusão

Spring Security e Spring Boot permitem configurar rapidamente um servidor de autorização / autenticação OAuth2 completo de uma maneira quase declarativa. A configuração pode ser ainda mais reduzida configurando as propriedades do cliente OAuth2 diretamente em application.properties/yml arquivo, conforme explicado neste tutorial .

Todo o código-fonte está disponível neste repositório GitHub: spring-glee-o-meter . Um cliente Angular que consome os recursos publicados pode ser encontrado neste repositório GitHub: glee-o-meter .

python criar objeto com atributos

Compreender o básico

O que é OAuth2?

OAuth2 é uma estrutura de autorização para permitir que um aplicativo de terceiros obtenha acesso limitado a um serviço HTTP por meio do compartilhamento de um token de acesso. Sua especificação substitui e torna obsoleto o protocolo OAuth 1.0.

O que é JWT?

JWT significa JSON Web Token, uma especificação para a representação de reivindicações a serem transferidas entre duas partes. As declarações são codificadas como um objeto JSON usado como carga útil de uma estrutura criptografada que permite que as declarações sejam assinadas ou criptografadas digitalmente.

O que é Spring Security?

Spring Security é uma estrutura focada em fornecer autenticação e autorização para aplicativos baseados em Spring.

O que é Spring Boot?

Spring Boot é uma visão opinativa da plataforma Spring e bibliotecas de terceiros que permite minimizar a configuração de aplicativos baseados em Spring, mantendo o nível de qualidade de nível de produção.

O Futuro da UX é Nossa Humanidade

Design Ux

O Futuro da UX é Nossa Humanidade
Saudi Aramco vai gastar US $ 18 bilhões em crescimento nas Américas -Motiva

Saudi Aramco vai gastar US $ 18 bilhões em crescimento nas Américas -Motiva

Mundo

Publicações Populares
Linha de assunto - Como abordar o design de e-mail
Linha de assunto - Como abordar o design de e-mail
Gerentes de produto x Gerentes de projeto: entendendo as semelhanças e diferenças essenciais
Gerentes de produto x Gerentes de projeto: entendendo as semelhanças e diferenças essenciais
Tutorial de física de videogame - Parte II: Detecção de colisão para objetos sólidos
Tutorial de física de videogame - Parte II: Detecção de colisão para objetos sólidos
Estética e percepção - como abordar as imagens da experiência do usuário
Estética e percepção - como abordar as imagens da experiência do usuário
Escreva testes que importem: enfrente o código mais complexo primeiro
Escreva testes que importem: enfrente o código mais complexo primeiro
 
Como Facilitar a Mudança por meio da Liderança de Servo Ágil
Como Facilitar a Mudança por meio da Liderança de Servo Ágil
O lendário alpinista Jim Bridwell morre aos 73 anos
O lendário alpinista Jim Bridwell morre aos 73 anos
Tutorial Grape Gem: como construir uma API semelhante a REST em Ruby
Tutorial Grape Gem: como construir uma API semelhante a REST em Ruby
Energia híbrida: vantagens e benefícios da vibração
Energia híbrida: vantagens e benefícios da vibração
Dê a eles incentivos - como aproveitar uma estrutura de design de programa de fidelidade
Dê a eles incentivos - como aproveitar uma estrutura de design de programa de fidelidade
Publicações Populares
  • estrutura típica de fundos de private equity
  • aprenda c ++ da maneira mais difícil
  • qual é o princípio da proximidade
  • melhores práticas de teste de unidade java
  • escrever documentação técnica para software
Categorias
  • Receita E Crescimento
  • Aprendendo
  • Processo Interno
  • Processo De Design
  • © 2022 | Todos Os Direitos Reservados

    portaldacalheta.pt