OAuth2 e OIDC profissional com Java e Spring: primeiros passos

    Foto do autor Nelio Alves
    Nelio Alves
    Compartilhar
    Compartilhar no LinkedInCompartilhar no FacebookCompartilhar no XCompartilhar no WhatsApp
    Imagem banner do post

    A maioria dos projetos modernos delega autenticação para serviços externos como Auth0, Okta ou Keycloak - e não há nada de errado nisso, é o caminho mais curto para começar. O problema é que, sem entender o que está acontecendo por baixo, fica difícil tomar decisões informadas: como integrar com um SPA? Por que o ID token não deveria carregar o email do usuário? Quando preciso de Authorization Code + PKCE e quando Client Credentials basta? O Spring Authorization Server (Spring AS) é a peça que permite construir o seu próprio AS em Java/Spring, expondo os endpoints OAuth2/OIDC padrão (/oauth2/authorize, /oauth2/token, /userinfo, /.well-known/openid-configuration, etc.) com todos os ajustes que você quiser. Neste post, partindo de uma baseline mínima in-memory, vamos evoluir um AS educativo (dsbooks-as) em cinco passos incrementais até chegar a um modelo persistido em banco, com login customizado, multi-clients cadastrados, e um modelo de usuário OIDC-compliant que respeita LGPD/GDPR - tudo sem mágica de bibliotecas terceiras.

    Passo 1: externalizar valores e reorganização do Postman

    Conforme mencionado, vamos partir de uma implementação baseline onde temos um Authorization Server mínimo com recursos in-memory, capaz executar o fluxo de login e emitir tokens. Esta baseline foi explorada em outro post deste blog, que serviu para compreendermos em detalhes como funciona o fluxo authorization code do OAuth2.

    A baseline tem o issuer URL do AS hardcoded dentro de AuthorizationServerConfig. Antes de mexer em qualquer coisa estrutural, vale "limpar a casa" para o aluno aprender o padrão de configuração externalizada que será reutilizado em todo o resto.

    Apenas o issuer URL vai para o application.properties neste passo. As demais configurações, tais como usuários e clients, não entram aqui porque vão direto para o banco nos passos mais adiante.

    O application.properties final deste passo fica assim:

    Copiar
    spring.profiles.active=${APP_PROFILE:test}
    
    spring.jpa.open-in-view=false
    
    server.port=${SERVER_PORT:9000}
    server.servlet.session.cookie.name=AS_DSBOOKS_SESSION
    
    app.security.issuer=${APP_ISSUER_URL:http://localhost:9000}

    E em AuthorizationServerConfig:

    Copiar
    @Value("${app.security.issuer}")
    private String issuer;
    
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
            .issuer(issuer)
            .build();
    }

    A segunda parte deste passo é reorganizar a coleção do Postman para ser sustentável. Em vez de variáveis presas dentro da própria collection (que viajam quando você exporta/compartilha), criamos um environment chamado dsbooks-dev, em arquivo separado na raiz do projeto:

    VariávelValor
    as-hosthttp://localhost:9000
    client-idmanual-client
    client-secretmy-client-secret
    scopesopenid profile email
    redirect-urihttps://oauth.pstmn.io/v1/callback
    code-verifierdBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    statexyz

    Na collection, o fluxo OAuth2 (Authorization Code com PKCE) passa a ser configurado na aba Authorization da collection inteira - clicar em "Get New Access Token" dispara o fluxo completo. As requisições individuais ficam apenas com o que importa: GET OIDC Discovery Document, GET OAuth2 AS Metadata, GET JWK Set, GET UserInfo. As três primeiras usam No Auth (são endpoints públicos); o UserInfo herda a autorização da collection (Inherit auth from parent).

    O ganho não é só estético: ao apontar a collection para outra instância do AS (ex.: ambiente de homologação), basta trocar de environment - nada na collection precisa ser editado.

    Passo 2: formulário de login customizado com Thymeleaf

    O formLogin(Customizer.withDefaults()) da baseline mostra a tela de login genérica do Spring Security. Para um AS de verdade, queremos identidade visual própria - afinal, é a única tela que o usuário final realmente vê do AS.

    Adicionamos a dependência:

    Copiar
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    E criamos um controller mínimo só para mapear GET /login para o template:

    Copiar
    @Controller
    public class LoginController {
    
        @GetMapping("/login")
        public String login() {
            return "login";
        }
    }

    O template (src/main/resources/templates/login.html) carrega CSS externo, mostra o título "Login com DSBooks", trata mensagens de erro/logout via ${param.error} e ${param.logout}, e tem dois cuidados específicos:

    • O atributo autocomplete="new-password" no input de senha. Evita que o navegador injete senhas previamente memorizadas no momento em que o usuário clica no campo - comum em telas de login compartilhadas com outros formulários do mesmo domínio.
    • Um botão de toggle de visualização da senha, com dois SVGs inline (eye / eye-off, estilo feather-icons). A visibilidade dos dois é controlada por uma classe CSS (.is-hidden), e um JS pequeno alterna entre type="password" e type="text" ao clicar.

    O CSS adota dark mode usando variáveis CSS no :root:

    Copiar
    :root {
        --color-bg:        #0e1116;
        --color-surface:   #161b22;
        --color-border:    #30363d;
        --color-text:      #e6edf3;
        --color-primary:   #58a6ff;
        --space-md:        16px;
        --radius-md:       8px;
        /* ... etc ... */
    }

    A vantagem das variáveis é que mudar a paleta inteira ou criar um modo claro depois passa a ser trivial - uma única regra :root (ou [data-theme="light"]) e tudo segue junto.

    No defaultSecurityFilterChain, três alterações: apontar para a página customizada, liberar /login, /css/**, /js/** para acesso público, e remover o formLogin(Customizer.withDefaults()) no lugar de:

    Copiar
    .formLogin(form -> form
        .loginPage("/login")
        .permitAll()
    );

    Detalhe importante sobre CSRF: como o template usa th:action, o RequestDataValueProcessor do Spring automaticamente injeta o input hidden do _csrf no <form>. Não escreva manualmente o <input name="_csrf" ...> - você acaba com dois (um do Thymeleaf, outro seu) e a IDE não reclama.

    Passo 3: persistir usuários no banco de dados

    A baseline usa InMemoryUserDetailsManager com Maria e Alex hardcoded. É hora de mover para JPA + H2.

    Três dependências entram no pom.xml:

    Copiar
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-h2console</artifactId>
    </dependency>

    A última merece atenção: no Spring Boot 4 o H2ConsoleAutoConfiguration foi extraído para um módulo próprio (spring-boot-h2console). Sem essa dependência, a propriedade spring.h2.console.enabled=true é silenciosamente ignorada e o console nunca é registrado.

    A entidade nesta etapa é deliberadamente simples - só id, username (será trocado por email no Passo 5) e password. Implementa UserDetails com getAuthorities() retornando List.of(), porque nosso AS não fará controle de roles/authorities do usuário:

    Copiar
    @Entity
    @Table(name = "tb_user")
    public class UserEntity implements UserDetails {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(unique = true, nullable = false)
        private String username;
    
        @Column(nullable = false)
        private String password;
    
        // getters/setters omitidos
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return List.of();
        }
    }

    Repository é JpaRepository<UserEntity, Long> com findByUsername. O service implementa UserDetailsService, que é o componente padrão do Spring Security para recuperar usuários por meio do método UserDetails loadUserByUsername(String username):

    Copiar
    @Service
    public class UserService implements UserDetailsService {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) {
            return userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        }
    }

    O seed dos usuários é programático, via CommandLineRunner - não via import.sql. A motivação não é o encoding da senha (que pode tranquilamente ser {noop}12345678 em SQL), e sim antecipatória: um redesign futuro pode introduzir criptografia simétrica em campos sensíveis (email, telefone), via JPA AttributeConverter. Como o ciphertext muda a cada execução (vetor de inicialização aleatório), não dá para hardcodar valores em SQL - o seed precisa rodar em código Java, com os converters JPA ativos. Adotando CommandLineRunner desde já evitamos retrabalho.

    Copiar
    @Configuration
    public class DatabaseSeeder implements CommandLineRunner {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public void run(String... args) {
            if (userRepository.count() > 0) return;
            seedUser("maria@example.com", "{noop}12345678");
            seedUser("alex@example.com",  "{noop}12345678");
        }
    
        private void seedUser(String username, String password) {
            UserEntity user = new UserEntity();
            user.setUsername(username);
            user.setPassword(password);
            userRepository.save(user);
        }
    }

    Para o H2 console funcionar acessível pelo navegador, três ajustes no defaultSecurityFilterChain por causa de uma particularidade do Spring Security 7: o requestMatchers(String...) padrão resolve para um MvcRequestMatcher, que só casa rotas servidas pelo DispatcherServlet - e o H2 console é um servlet separado. Solução: usar PathPatternRequestMatcher explicitamente. Além disso, o console envia POST sem CSRF token e usa <iframe>:

    Copiar
    PathPatternRequestMatcher h2Console = PathPatternRequestMatcher.pathPattern("/h2-console/**");
    
    http
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/login", "/css/**", "/js/**").permitAll()
            .requestMatchers(h2Console).permitAll()
            .anyRequest().authenticated()
        )
        .csrf(csrf -> csrf.ignoringRequestMatchers(h2Console))
        .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
        .formLogin(...)

    O AntPathRequestMatcher que aparece em tutoriais antigos foi removido no Spring Security 7 - o substituto é o PathPatternRequestMatcher.

    Com isso, o InMemoryUserDetailsManager sai do AuthorizationServerConfig, e o login continua funcionando exatamente igual - só que agora os usuários vêm do H2.

    Passo 4: persistir clients no banco de dados

    O manual-client que a gente usou no Passo 1 ainda está hardcoded num bean RegisteredClient dentro do AuthorizationServerConfig. Hora de mover para o banco e, de quebra, cadastrar os múltiplos clients que um ecossistema real costuma ter.

    A entidade OAuthClient (tabela tb_oauth_client) representa um RegisteredClient do Spring AS. Decisão deliberada: as coleções (redirect_uris, scopes, grant_types, auth_methods, post_logout_redirect_uris) ficam como strings separadas por vírgula num único campo cada, em vez de tabelas filhas. Mantém o esquema simples e suficiente para volumes pequenos:

    Copiar
    @Entity
    @Table(name = "tb_oauth_client")
    public class OAuthClient {
    
        @Id
        private String id;
    
        @Column(unique = true, nullable = false)
        private String clientId;
    
        // Pode ser null para public clients (sem secret)
        private String clientSecret;
    
        @Column(nullable = false)
        private String clientName;
    
        /** Redirect URIs separadas por vírgula */
        @Column(nullable = false, length = 1000)
        private String redirectUris;
    
        /** Post-logout redirect URIs separadas por vírgula (null = nenhuma) */
        @Column(length = 1000)
        private String postLogoutRedirectUris;
    
        /** Scopes separados por vírgula — ex: "openid,profile,email" */
        @Column(nullable = false, length = 500)
        private String scopes;
    
        /** Grant types separados por vírgula — ex: "authorization_code,refresh_token" */
        @Column(nullable = false, length = 200)
        private String authorizationGrantTypes;
    
        /** Métodos de autenticação do client separados por vírgula */
        @Column(nullable = false, length = 200)
        private String clientAuthenticationMethods;
    
        /** Validade do access token em segundos (default: 1 hora) */
        @Column(nullable = false)
        private long accessTokenTtlSeconds = 3600L;
    
        /** Validade do refresh token em segundos (default: 30 dias) */
        @Column(nullable = false)
        private long refreshTokenTtlSeconds = 2592000L;
    
        /** Se exige tela de consent para o usuário. False para apps first-party. */
        @Column(nullable = false)
        private boolean requireConsent = true;
    
        // ...
    }

    Para o Spring AS enxergar esses dados como RegisteredClient, criamos uma implementação custom de RegisteredClientRepository. Ela carrega a entidade via JPA e converte de/para o modelo do framework:

    Copiar
    @Component
    public class CustomRegisteredClientRepository implements RegisteredClientRepository {
    
        @Autowired
        private OAuthClientRepository clients;
    
        @Override
        public RegisteredClient findByClientId(String clientId) {
            return clients.findByClientId(clientId).map(this::toDomain).orElse(null);
        }
    
        private RegisteredClient toDomain(OAuthClient e) {
            RegisteredClient.Builder b = RegisteredClient.withId(e.getId())
                .clientId(e.getClientId())
                .clientSecret(e.getClientSecret())
                .clientName(e.getClientName());
    
            Arrays.stream(e.getAuthorizationGrantTypes().split(","))
                .map(String::trim)
                .forEach(t -> b.authorizationGrantType(new AuthorizationGrantType(t)));
            // (e assim por diante para auth methods, redirect URIs, scopes...)
    
            b.tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofSeconds(e.getAccessTokenTtlSeconds()))
                .refreshTokenTimeToLive(Duration.ofSeconds(e.getRefreshTokenTtlSeconds()))
                .build());
    
            return b.build();
        }
        // toEntity() para o save() inverso
    }

    O seed dos clients vai via import.sql (clients não têm campos sensíveis a serem encriptados, então SQL basta - diferente do seed de usuários). Três clients são cadastrados, representando atores reais distintos:

    ClientTipoPara quê
    my-client-idAuth Code + Refresh, sem consentApp Next.js de catálogo de livros
    my-backendClient Credentials, só client_secret_basicBackend Spring para chamadas m2m (introspecção, jobs, m2m com outros serviços)
    manual-clientAuth Code + RefreshTestes manuais no Postman

    Por que três clients separados? Cada um tem seu redirect URI legítimo e papel semântico próprio. Manter manual-client separado de my-client-id evita que o callback do Postman fique whitelisted num client de produção (vetor de ataque clássico). E em logs/auditoria, o aud do JWT fica autoexplicativo: my-client-id = tráfego de usuário real; manual-client = teste manual; my-backend = m2m.

    Como o manual-client faz só o fluxo de usuário (Auth Code), o Postman precisa de uma forma adicional para testar m2m. Adicionamos três variáveis ao environment (backend-client-id, backend-client-secret, backend-access-token) e uma nova requisição POST Token (Client Credentials) dentro da pasta Auth:

    • Auth: HTTP Basic com username = {{backend-client-id}}, password = {{backend-client-secret}} (my-backend aceita só client_secret_basic).
    • Body (form-urlencoded): grant_type=client_credentials, scope=openid.
    • Test script (post-response): salva access_token em backend-access-token.

    Quando precisar testar uma rota protegida estilo m2m, basta sobrescrever o Authorization da request específica com Bearer {{backend-access-token}}.

    Passo 5: redesign do usuário conforme OIDC

    Até aqui, o sub dos tokens emitidos pelo AS é o próprio email do usuário - porque getUsername() retorna o email e o Spring AS usa isso como subject por padrão. Isso é uma violação de LGPD/GDPR: dado pessoal trafegando em tokens que circulam por sistemas intermediários, gateways, logs estruturados e até mesmo aparecendo em URLs de fluxos OAuth2.

    A correção é redesenhar a entidade conforme os scopes do OIDC: sub vira um UUID opaco, e dados pessoais ficam disponíveis apenas via /userinfo, sob demanda.

    A nova UserEntity:

    Copiar
    @Entity
    @Table(name = "tb_user")
    public class UserEntity implements UserDetails {
    
        // Internos
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        @Column(nullable = false) private String password;
    
        // Scope openid
        @Column(unique = true, nullable = false)
        private String sub;       // UUID - não vaza dado pessoal
    
        // Scope profile
        private String name;
        private String picture;
    
        // Scope email
        @Column(unique = true, nullable = false)
        private String email;
        @Column(name = "email_verified", nullable = false)
        private boolean emailVerified = false;
    
        @Override
        public String getUsername() {
            return email;   // UserDetails exige um identificador; email é o que o usuário digita
        }
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return List.of();
        }
        // ...
    }

    O UserService.loadUserByUsername agora busca por email; o seed gera UUIDs fixos (para facilitar debug) e popula nome/foto/email_verified; e o template login.html muda o input para name="email". Em defaultSecurityFilterChain, adicionamos .usernameParameter("email") no formLogin para Spring Security ler o campo correto.

    Faltam dois componentes para fechar o redesign:

    1. Sobrescrever o sub dos tokens. Por padrão o Spring AS usa Authentication.getName() como subject - que, no nosso caso, é o email. Precisamos que seja o UUID. Um OAuth2TokenCustomizer<JwtEncodingContext> resolve:

    Copiar
    @Component
    public class JwtSubClaimCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
    
        @Override
        public void customize(JwtEncodingContext context) {
            Object principal = context.getPrincipal().getPrincipal();
            if (principal instanceof UserEntity user) {
                context.getClaims().subject(user.getSub());
            }
            // Para Client Credentials (principal = RegisteredClient), o sub natural
            // já é o client_id - não precisa alterar.
        }
    }

    O Spring AS detecta automaticamente esse bean e o aplica ao montar todo JWT (access token e ID token).

    2. Servir email/name/picture só via /userinfo. A esta altura, o ID token e o access token já estão limpos (só sub UUID + claims técnicos como iss, aud, exp). Mas precisamos que /userinfo retorne os dados pessoais quando o client tiver os scopes apropriados. Implementamos um userInfoMapper customizado:

    Copiar
    @Component
    public class CustomOidcUserInfoMapper
            implements Function<OidcUserInfoAuthenticationContext, OidcUserInfo> {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public OidcUserInfo apply(OidcUserInfoAuthenticationContext context) {
            OAuth2Authorization authorization = context.getAuthorization();
            Set<String> scopes = context.getAccessToken().getScopes();
            String email = authorization.getPrincipalName();
    
            UserEntity user = userRepository.findByEmail(email)
                .orElseThrow(() -> new OAuth2AuthenticationException(
                    new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
                        "User no longer exists; token is stale", null)));
    
            OidcUserInfo.Builder b = OidcUserInfo.builder().subject(user.getSub());
            if (scopes.contains(OidcScopes.PROFILE)) {
                if (user.getName() != null)    b.name(user.getName());
                if (user.getPicture() != null) b.picture(user.getPicture());
            }
            if (scopes.contains(OidcScopes.EMAIL)) {
                b.email(user.getEmail());
                b.emailVerified(user.isEmailVerified());
            }
            return b.build();
        }
    }

    E plugamos no filter chain:

    Copiar
    .with(authorizationServerConfigurer, as -> as
        .oidc(oidc -> oidc
            .userInfoEndpoint(userInfo -> userInfo
                .userInfoMapper(userInfoMapper)
            )
        )
    )

    Detalhe sobre o tratamento de "user not found": no fluxo do /userinfo, o Spring AS já valida assinatura/expiração do token antes de chamar o mapper - token inválido vira 401 sozinho. O cenário de user inexistente só acontece se o usuário foi deletado (ou teve email mudado) entre o login e a chamada ao /userinfo. Lançar OAuth2AuthenticationException com invalid_token faz o Spring AS responder 401 com o header WWW-Authenticate: Bearer error="invalid_token" - semanticamente correto, dizendo ao cliente "esse token é stale, refaça login".

    Ao final do passo, decodificando os tokens emitidos, vê-se que tanto o access_token quanto o id_token carregam apenas:

    Copiar
    {
        "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "aud": "manual-client",
        "iss": "http://localhost:9000",
        "scope": ["openid", "profile", "email"],
        "exp": 1777334129,
        ...
    }

    E o /userinfo com Bearer token (e os 3 scopes) responde:

    Copiar
    {
        "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "name": "Maria Silva",
        "picture": "https://i.pravatar.cc/300?img=1",
        "email": "maria@example.com",
        "email_verified": false
    }

    Mesmo sub em todos os lugares; dado pessoal só onde precisa estar.

    Projeto para baixar

    Você pode ver o projeto correspondente a este post no Github da Devsuperior, no branch aula-primeiros-passos:

    https://github.com/devsuperior/dsbooks-as

    Conclusão

    Saímos de um Authorization Server mínimo, in-memory, com tela de login default e um único client hardcoded - e chegamos em um AS persistido em banco H2, com formulário customizado em dark mode, três clients de papéis distintos cadastrados, e um modelo de usuário OIDC-compliant que protege dados pessoais. Mais importante que o resultado: cada passo introduziu uma única ideia nova, deixando claro o porquê de cada peça. Externalizar configs antes de mexer em qualquer coisa estrutural. Trocar UI antes de mexer em infra. Pôr usuários no banco antes de pôr clients. E só no final, quando todo o resto está estável, encarar o redesign do modelo de identidade - que é onde os erros de LGPD/GDPR mais aparecem em projetos reais.

    O AS resultante é educativo, mas ainda há vários passos para se tornar profissional. Os principais que ficam de fora deste post:

    • Persistência das OAuth2Authorization no banco. Hoje elas vivem em memória - um restart invalida todos os refresh tokens em circulação. Em produção isso é inaceitável: implementar OAuth2AuthorizationService e OAuth2AuthorizationConsentService baseados em JPA é o próximo passo natural.
    • Persistência das chaves RSA. Hoje o JWKSource gera um novo par a cada startup, o que invalida todos os tokens emitidos antes. Em produção: chave privada armazenada (idealmente cifrada com chave mestra) e rotacionada de forma controlada via uma tabela tb_signing_key.
    • Login federado (Google, GitHub, IdP corporativo). O Spring Security já entrega quase tudo via OAuth2LoginConfigurer; o que dá trabalho é mapear o FederatedUser para a sua UserEntity local e cuidar do logout em cascata para o IdP upstream.
    • Logout corretamente cascateado. O OIDC define um end_session_endpoint; cliente faz logout, AS limpa sessão local e - se houver IdP federado - repassa o logout adiante.
    • Auditoria estruturada. Cada login, cada token emitido, cada /userinfo acessado, cada falha de autenticação deveria gerar um registro estruturado para SIEM/forense. Spring AS tem hooks (AuthenticationSuccessHandler, AuthenticationFailureHandler, response handlers do token endpoint) que facilitam.
    • Encriptação de campos sensíveis na UserEntity (email, telefone, CPF). O CommandLineRunner que adotamos no Passo 3 já está preparado para isso - falta o AttributeConverter em si.
    • Consent screen para clients third-party. Os três clients deste post são todos first-party (sem consent), mas em um AS público você quer mostrar a tela "App X quer acessar seu nome e email - autorizar?".
    • CRUD de usuário com revogação ativa de tokens. Ao deletar um usuário, todos os refresh tokens em circulação para aquele principal precisam ser invalidados na hora.

    A boa notícia: cada um desses pontos é uma evolução natural da arquitetura que construímos aqui - não um redesign. Você pode parar em qualquer ponto e ter um AS que funciona, em vez de ficar olhando para um único monolito gigante e tentando entender tudo de uma vez. É essa a diferença entre estudar OAuth2/OIDC pela leitura das RFCs e estudar pelo código que evolui passo a passo.

    Foto do autor Nelio Alves
    Nelio Alves
    Desenvolvedor e Professor
    Olá, meu nome é Nelio Alves. Sou graduado em Ciência da Computação e possuo mestrado e doutorado em Engenharia de Software pela Universidade Federal de Uberlândia. Trabalho como desenvolvedor e professor de programação há mais de 20 anos, e sou um dos educadores de tecnologia mais influentes da Internet com mais de 500 mil alunos online.