Parte 2: Adicionando Usuários ao Sistema de Login com Google

Introdução Após configurar o login com Google na Parte 1, o próximo passo natural é persistir os dados dos usuários em um banco de dados para uso posterior, como personalização ou rastreamento. Persistir usuários autenticados é uma prática comum em aplicações web, permitindo que informações como email e nome sejam armazenadas e reutilizadas sem depender exclusivamente dos tokens retornados pelo provedor de identidade. Historicamente, isso evoluiu com o crescimento das aplicações SaaS, onde dados de usuários passaram a ser fundamentais para funcionalidades específicas. Nesta parte, vamos adicionar uma camada de persistência ao nosso sistema de login com Google, usando o Spring Data JPA e o banco H2 em memória. O objetivo é salvar os dados do usuário (como sub, email e name) na primeira autenticação, mantendo o código simples e aproveitando ao máximo as ferramentas do Spring, como JpaRepository e OidcUserService. Configuração Inicial Atualize as dependências do projeto, adicionando: spring-boot-starter-data-jpa: Inclui Hibernate e Spring Data para persistência. h2: Banco em memória para simplificar o tutorial. pom.xml org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime Configure o banco H2 em memória e JPA, adicionando: datasource: Configura o H2 em memória com testdb como nome do banco. jpa.hibernate.ddl-auto: update: O Hibernate cria ou atualiza a tabela users com base na entidade. application.yml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update Resumo do Fluxo Geral Autenticação: O usuário faz login via Google, redirecionado por /oauth2/authorization/google. Callback: O Spring processa o código retornado em /login/oauth2/code/google e chama CustomOAuth2UserService. Persistência: O CustomOAuth2UserService verifica se o usuário existe no banco e, se não, o cria usando UserRepository. Resposta: O HomeController retorna o JWT e uma mensagem, sem alterações relacionadas a persistência. Passo a Passo Vamos criar a entidade que mapeará os Usuários: @Entity e @Table: Define a entidade mapeada para a tabela users. Aqui anotação @Table é importante pois ter uma entidade com o nome User poderá causar conflitos com o banco de dados. @Id: Marca id como chave primária (preenchida com o sub do Google). @Column(name = "tenant_id", nullable = false): Inclui a coluna tenant_id, que será usada na Parte 3 para multi-tenancy. Por ora, será preenchida com o mesmo valor do id. E o seu repositório que nos fornece já métodos prontos de persistência: JpaRepository: Fornece métodos como findById, save, etc., usando String como tipo da chave. findByEmail: Método derivado opcional, mas não essencial neste momento. 1. Criar a Entidade User import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "users") public class User { @Id private String id; @Column(name = "email") private String email; @Column(name = "name") private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 2. Criar o Repositório (UserRepository) import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByEmail(String email); // Método extra, não usado aqui } Agora criaremos nosso serviço customizado CustomOAuth2UserService que realiza a persistência e recuperação do usuário do banco de dados: loadUser(OidcUserRequest userRequest): super.loadUser(): Executa o fluxo OIDC padrão, obtendo dados do user-info-uri do Google. getSubject(), getEmail(), getFullName(): Extrai os atributos do OidcUser. findById(id).orElseGet(): Busca o usuário pelo id (o sub); se não existir, cria um novo e salva no banco via save(). SQL Gerado: Algo como INSERT INTO users (id, email, name) VALUES (?, ?, ?). Retorna o OidcUser para o Spring Security continuar o fluxo de autenticação. 3. Criar o CustomOAuth2UserService import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oa

Mar 28, 2025 - 03:42
 0
Parte 2: Adicionando Usuários ao Sistema de Login com Google

Introdução

Após configurar o login com Google na Parte 1, o próximo passo natural é persistir os dados dos usuários em um banco de dados para uso posterior, como personalização ou rastreamento.

Persistir usuários autenticados é uma prática comum em aplicações web, permitindo que informações como email e nome sejam armazenadas e reutilizadas sem depender exclusivamente dos tokens retornados pelo provedor de identidade. Historicamente, isso evoluiu com o crescimento das aplicações SaaS, onde dados de usuários passaram a ser fundamentais para funcionalidades específicas.

Nesta parte, vamos adicionar uma camada de persistência ao nosso sistema de login com Google, usando o Spring Data JPA e o banco H2 em memória. O objetivo é salvar os dados do usuário (como sub, email e name) na primeira autenticação, mantendo o código simples e aproveitando ao máximo as ferramentas do Spring, como JpaRepository e OidcUserService.

Configuração Inicial

Atualize as dependências do projeto, adicionando:

  • spring-boot-starter-data-jpa: Inclui Hibernate e Spring Data para persistência.
  • h2: Banco em memória para simplificar o tutorial.
pom.xml
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            com.h2database
            h2
            runtime
        

Configure o banco H2 em memória e JPA, adicionando:

  • datasource: Configura o H2 em memória com testdb como nome do banco.
  • jpa.hibernate.ddl-auto: update: O Hibernate cria ou atualiza a tabela users com base na entidade.
application.yml
spring:  
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update

Resumo do Fluxo Geral

  1. Autenticação: O usuário faz login via Google, redirecionado por /oauth2/authorization/google.
  2. Callback: O Spring processa o código retornado em /login/oauth2/code/google e chama CustomOAuth2UserService.
  3. Persistência: O CustomOAuth2UserService verifica se o usuário existe no banco e, se não, o cria usando UserRepository.
  4. Resposta: O HomeController retorna o JWT e uma mensagem, sem alterações relacionadas a persistência.

Passo a Passo

Vamos criar a entidade que mapeará os Usuários:

  • @Entity e @Table: Define a entidade mapeada para a tabela users. Aqui anotação @Table é importante pois ter uma entidade com o nome User poderá causar conflitos com o banco de dados.
  • @Id: Marca id como chave primária (preenchida com o sub do Google).
  • @Column(name = "tenant_id", nullable = false): Inclui a coluna tenant_id, que será usada na Parte 3 para multi-tenancy. Por ora, será preenchida com o mesmo valor do id.

E o seu repositório que nos fornece já métodos prontos de persistência:

  • JpaRepository: Fornece métodos como findById, save, etc., usando String como tipo da chave.
  • findByEmail: Método derivado opcional, mas não essencial neste momento.
1. Criar a Entidade User
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {
    @Id
    private String id;

    @Column(name = "email")
    private String email;

    @Column(name = "name")
    private String name;

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}
2. Criar o Repositório (UserRepository)
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, String> {
    Optional<User> findByEmail(String email); // Método extra, não usado aqui
}

Agora criaremos nosso serviço customizado CustomOAuth2UserService que realiza a persistência e recuperação do usuário do banco de dados:

  • loadUser(OidcUserRequest userRequest):
    • super.loadUser(): Executa o fluxo OIDC padrão, obtendo dados do user-info-uri do Google.
    • getSubject(), getEmail(), getFullName(): Extrai os atributos do OidcUser.
    • findById(id).orElseGet(): Busca o usuário pelo id (o sub); se não existir, cria um novo e salva no banco via save().
    • SQL Gerado: Algo como INSERT INTO users (id, email, name) VALUES (?, ?, ?).
    • Retorna o OidcUser para o Spring Security continuar o fluxo de autenticação.
3. Criar o CustomOAuth2UserService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

@Service
public class CustomOAuth2UserService extends OidcUserService {
    private final UserRepository userRepository;

    @Autowired
    public CustomOAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = super.loadUser(userRequest);
        String id = oidcUser.getSubject();
        String email = oidcUser.getEmail();
        String name = oidcUser.getFullName();

        userRepository.findById(id)
            .orElseGet(() -> {
                User newUser = new User();
                newUser.setId(id);
                newUser.setEmail(email);
                newUser.setName(name);
                return userRepository.save(newUser);
            });

        return oidcUser;
    }
}

Agora podemos atualizar nossas configurações de segurança retirando o método oidcUserService() e injetando nosso CustomOAuth2UserService:

  • securityFilterChain(HttpSecurity http): Integra o customOAuth2UserService para processar o usuário após o login.
  • Nota: Mantém a estrutura da Parte 1, mas agora usa o serviço customizado.
4. Atualizar o SecurityConfig
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/oauth2/**", "/login/oauth2/**", "/error", "/favicon.ico", "/").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/home", true)
                .failureUrl("/error?error=true")
                // Atualização do oidcUserService
                .userInfoEndpoint(userInfo -> userInfo.oidcUserService(customOAuth2UserService))
            )
            .logout(logout -> logout.logoutSuccessUrl("/").permitAll())
            .build();
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        JWKSource<SecurityContext> jwkSource = getJwkSource();
        return new NimbusJwtEncoder(jwkSource);
    }

    private JWKSource<SecurityContext> getJwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private KeyPair generateRsaKey() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException("Erro ao gerar RSA KeyPair", ex);
        }
    }
}
5. Sem alterações em HomeController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestController
public class HomeController {

    @Autowired
    private JwtEncoder jwtEncoder;

    @GetMapping("/home")
    public ResponseEntity<Map<String, String>> home(@AuthenticationPrincipal OidcUser oidcUser) {
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("self")
            .issuedAt(now)
            .expiresAt(now.plusSeconds(3600))
            .subject(oidcUser.getSubject())
            .claim("email", oidcUser.getEmail())
            .claim("name", oidcUser.getFullName())
            .build();

        String jwt = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();

        Map<String, String> response = new HashMap<>();
        response.put("message", "Welcome " + oidcUser.getFullName());
        response.put("jwt", jwt);
        response.put("message", "Welcome " + oidcUser.getGivenName());
        response.put("email", oidcUser.getEmail());
        response.put("givenName", oidcUser.getGivenName());
        response.put("familyName", oidcUser.getFamilyName());
        response.put("picture", oidcUser.getPicture());
        response.put("emailVerified", oidcUser.getEmailVerified().toString());
        response.put("issuer", oidcUser.getIssuer().toString());
        response.put("issuedAt", oidcUser.getIssuedAt().toString());
        response.put("expiresAt", oidcUser.getExpiresAt().toString());
        return ResponseEntity.ok(response);
    }

    @GetMapping("/")
    public String root() {
        return "Bem-vindo à página inicial!";
    }
}
6. Testar o Sistema
  • Execute mvn spring-boot:run.
  • Acesse http://localhost:8080/home.
  • Após login, o usuário é salvo no H2 e você verá algo como:
  {
    "message": "Welcome Uira Teste",
    "jwt": "eyJraWQiOiJhYjNkZGYy..."
  }

Para conferir se os dados foram persistidos no banco de dados, basta criar um simples endpoint que retorna a lita de usuários:

7. Sem alterações em HomeController
@RestController
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/users")
    public List<User> users(@AuthenticationPrincipal OidcUser oidcUser) {
        return userRepository.findAll();
    }
}
  • Acesse http://localhost:8080/users.
  • Você verá a listagem dos usuários que foram salvos no banco de dados:
[
  {
    "id": "8547986604334038964715",
    "email": "teste@gmail.com",
    "name": "teste teste"
  }
]

Dica Bônus

Para ver os logs das operações realizadas pelo hibernate e do fluxo realizado entre o Spring e o Google, basta adicionar estas configurações ao application.yml:

application.yml
spring:
  jpa:
    properties:
      hibernate:
        format_sql: true
        show_sql: true

logging:
  level:
    org.springframework.security: DEBUG

No console da aplicação você poderá ver algo como:

2025-03-27T23:08:58.449-03:00 DEBUG 19672 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /home
2025-03-27T23:08:58.450-03:00 DEBUG 19672 --- [nio-8080-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2025-03-27T23:08:58.451-03:00 DEBUG 19672 --- [nio-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/home?continue to session
2025-03-27T23:08:58.451-03:00 DEBUG 19672 --- [nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], Not [And [Or [Ant [pattern='/login'], Ant [pattern='/favicon.ico']], And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@38f24018, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]]], org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer$$Lambda/0x0000027fca963ce8@65fa0784]
2025-03-27T23:08:58.451-03:00 DEBUG 19672 --- [nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@53cd9cb6
2025-03-27T23:08:58.451-03:00 DEBUG 19672 --- [nio-8080-exec-1] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/oauth2/authorization/google
2025-03-27T23:08:58.455-03:00 DEBUG 19672 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorization/google
2025-03-27T23:08:58.456-03:00 DEBUG 19672 --- [nio-8080-exec-3] o.s.s.web.DefaultRedirectStrategy        : Redirecting to https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=990849046214-2a6587e3mlrdiq9sjg3eist47cglr601.apps.googleusercontent.com&scope=openid%20profile%20email&state=irMOadAkAJlWOghuSv_h1dnHYoyHR3TN4vJN8lRj2Pc%3D&redirect_uri=http://localhost:8080/login/oauth2/code/google&nonce=JOBvBAx4-hWGrjEgZHk7Me0rqOHhfe1Pbj7PS_B9inc

Perceba o fluxo gerado pela aplicação:

  • Do filtro de segurança aplicado ao acessar /home: Securing GET /home
  • Até o redirecionamento para a página de login do Google: Redirecting to https://accounts.google
  • Passando pela verificação da existência do usuário e sua inclusão:
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name 
    from
        users u1_0 
    where
        u1_0.id=?

Hibernate: 
    insert 
    into
        users
        (email, name, id) 
    values
        (?, ?, ?)

Outras Abordagens Ao H2

  1. Persistência com Banco Externo (ex.: PostgreSQL):
    • Como: Configura um DataSource para PostgreSQL em vez de H2.
    • Vantagens: Persistência permanente, escalabilidade.
    • Desvantagens: Requer configuração adicional (ex.: Docker).
  2. Armazenamento em Cache (ex.: Redis):
    • Como: Usa Redis para salvar dados temporariamente em vez de um banco relacional.
    • Vantagens: Mais rápido, ideal para dados voláteis.
    • Desvantagens: Não persistente, menos adequado para dados críticos.

Agradeço por seguir até aqui e explorar como adicionar persistência de usuários ao nosso sistema de login com Google!