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>
Configure o banco H2 em memória e JPA, adicionando:
-
datasource
: Configura o H2 em memória comtestdb
como nome do banco. -
jpa.hibernate.ddl-auto: update
: O Hibernate cria ou atualiza a tabelausers
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 chamaCustomOAuth2UserService
. -
Persistência: O
CustomOAuth2UserService
verifica se o usuário existe no banco e, se não, o cria usandoUserRepository
. -
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 tabelausers
. Aqui anotação@Table
é importante pois ter uma entidade com o nomeUser
poderá causar conflitos com o banco de dados. -
@Id
: Marcaid
como chave primária (preenchida com osub
do Google). -
@Column(name = "tenant_id", nullable = false)
: Inclui a colunatenant_id
, que será usada na Parte 3 para multi-tenancy. Por ora, será preenchida com o mesmo valor doid
.
E o seu repositório que nos fornece já métodos prontos de persistência:
-
JpaRepository<User, String>
: Fornece métodos comofindById
,save
, etc., usandoString
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 douser-info-uri
do Google. -
getSubject()
,getEmail()
,getFullName()
: Extrai os atributos doOidcUser
. -
findById(id).orElseGet()
: Busca o usuário peloid
(osub
); se não existir, cria um novo e salva no banco viasave()
. -
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 ocustomOAuth2UserService
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
-
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).
-
Como: Configura um
-
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á! ⏳
Top comments (0)