DEV Community

Cover image for Parte 2: Adicionando Usuários ao Sistema de Login com Google
Uiratan Cavalcante
Uiratan Cavalcante

Posted on

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
        <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>
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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<User, String>: 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; }
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
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!";
    }
}
Enter fullscreen mode Exit fullscreen mode
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..."
  }
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        (?, ?, ?)
Enter fullscreen mode Exit fullscreen mode

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! 🙌

Foi um prazer detalhar cada passo e mostrar como o Spring Data JPA simplifica esse processo. O que achou dessa implementação com H2? 💡

Compartilhe suas impressões ou dúvidas nos comentários 📢 e, se curtiu, passe o tutorial adiante para quem possa se beneficiar 🌐.

Na Parte 3, vamos introduzir o multi-tenancy — nos vemos lá! ⏳

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay