Skip to content

Commit d83a038

Browse files
committed
fix(security): eliminate DB query on every request in JWT filter
Extract roles from JWT claims instead of loading UserDetails from database on each authenticated request. - Add extractRoles() to JwtService to read roles claim from token - Refactor JwtAuthenticationFilter to build Authentication from token claims only, removing userDetailsService dependency - Update LoanService.getAuthenticatedUser() to cast principal to String and query user by email - Add constructor overload to UserNotFoundException accepting email - Add unit tests for JwtService and JwtAuthenticationFilter
1 parent ce7289d commit d83a038

File tree

14 files changed

+278
-42
lines changed

14 files changed

+278
-42
lines changed

.env.dev

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ JWT_SECRET_KEY=dev-insecure-key-for-local-development-only-do-not-use-in-product
99
AWS_KEY=criar-access-key-na-aws
1010
AWS_SECRET=criar-secret-key-na-aws
1111
BUCKET_NAME=library-api-s3
12-
BUCKET_REGION=sa-east-1
12+
BUCKET_REGION=sa-east-1
13+
14+
ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans
15+
TRACING_SAMPLING_PROBABILITY=0.1 # 10% em prod — ajustável via env

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ SPRING_PROFILES_ACTIVE=prod
22
DB_URL=
33
DB_USERNAME=
44
DB_PASSWORD=
5-
JWT_SECRET_KEY=
65
REDIS_HOST=
76
REDIS_PORT=
7+
JWT_SECRET_KEY=
88

99
AWS_KEY=
1010
AWS_SECRET=
1111
BUCKET_NAME=
1212
BUCKET_REGION=
1313

14-
ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans
15-
TRACING_SAMPLING_PROBABILITY=0.1 # 10% em prod — ajustável via env
14+
ZIPKIN_ENDPOINT=
15+
TRACING_SAMPLING_PROBABILITY=

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gradle-wrapper.jar
1818
Thumbs.db
1919

2020
# Secrets
21-
#.env #arquivo liberado - projeto de estudos
21+
.env
2222
*.key
2323
*.pem
2424

readme_pdf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,8 +647,8 @@ Marca como `OVERDUE` empréstimos com `status = WAITING_RETURN` e `dueDate < hoj
647647
- [x] **Rate limiting** — Bucket4j ou Resilience4j
648648
- [x] **OpenTelemetry** — Tracing distribuído
649649
- [x] **Microservices** — Bounded contexts definidos, anticorrupção e schema per service implementados
650-
- [ ] **Deploy em cloud** — AWS ECS ou Render
651650
- [ ] **Extração Auth-Service** — primeiro serviço independente (menor e mais isolado)
651+
- [ ] **Deploy em cloud** — AWS ECS ou Render
652652
- [ ] **HATEOAS** — Hypermedia links
653653
- [ ] **WebSockets** — Notificações real-time de devolução
654654
- [ ] **Microservices** — Extração em serviços independentes

src/main/java/com/example/library/loan/LoanService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,13 @@ private Loan findWithItemsOrThrow(Long loanId) {
262262

263263
/**
264264
* Recupera o usuário autenticado direto do SecurityContext.
265-
* O principal já é um User (populado pelo JwtAuthenticationFilter),
266-
* então não precisa de uma query adicional ao banco.
265+
* O principal é o email (populado pelo JwtAuthenticationFilter),
267266
*/
268267
private User getAuthenticatedUser() {
269268
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
270-
return (User) auth.getPrincipal();
269+
String email = (String) auth.getPrincipal();
270+
return userLookupService.findByEmail(email)
271+
.orElseThrow(() -> new UserNotFoundException(email));
271272
}
272273

273274
/**

src/main/java/com/example/library/security/filter/JwtAuthenticationFilter.java

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
package com.example.library.security.filter;
22

33
import java.io.IOException;
4+
import java.util.List;
45

56
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
7+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
68
import org.springframework.security.core.context.SecurityContextHolder;
7-
import org.springframework.security.core.userdetails.UserDetails;
89
import org.springframework.stereotype.Component;
910
import org.springframework.web.filter.OncePerRequestFilter;
1011

1112
import com.example.library.security.jwt.JwtService;
12-
import com.example.library.user.CustomUserDetailsService;
1313

1414
import jakarta.servlet.FilterChain;
1515
import jakarta.servlet.ServletException;
1616
import jakarta.servlet.http.HttpServletRequest;
1717
import jakarta.servlet.http.HttpServletResponse;
18+
import lombok.RequiredArgsConstructor;
1819

20+
@RequiredArgsConstructor
1921
@Component
2022
public class JwtAuthenticationFilter extends OncePerRequestFilter {
2123

2224
private final JwtService jwtService;
23-
private final CustomUserDetailsService userDetailsService;
24-
25-
public JwtAuthenticationFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) {
26-
this.jwtService = jwtService;
27-
this.userDetailsService = userDetailsService;
28-
}
2925

3026
@Override
3127
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -40,11 +36,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4036
if (jwtService.isTokenValid(token)) {
4137

4238
String username = jwtService.extractUsername(token);
43-
44-
UserDetails user = userDetailsService.loadUserByUsername(username);
45-
46-
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null,
47-
user.getAuthorities());
39+
List<SimpleGrantedAuthority> authorities = jwtService.extractRoles(token)
40+
.stream()
41+
.map(SimpleGrantedAuthority::new)
42+
.toList();
43+
44+
UsernamePasswordAuthenticationToken authentication =
45+
new UsernamePasswordAuthenticationToken(username, null, authorities);
4846

4947
SecurityContextHolder.getContext().setAuthentication(authentication);
5048
}

src/main/java/com/example/library/security/jwt/JwtService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.time.Instant;
55
import java.time.temporal.ChronoUnit;
66
import java.util.Date;
7+
import java.util.List;
78

89
import javax.crypto.SecretKey;
910

@@ -82,6 +83,18 @@ public Instant getExpirationDate(String token) {
8283
public String extractUsername(String token) {
8384
return parseClaims(token).getSubject();
8485
}
86+
87+
public List<String> extractRoles(String token) {
88+
Claims claims = parseClaims(token);
89+
Object roles = claims.get("roles");
90+
if (roles instanceof List<?> list) {
91+
return list.stream()
92+
.filter(String.class::isInstance)
93+
.map(String.class::cast)
94+
.toList();
95+
}
96+
return List.of();
97+
}
8598

8699
public boolean isTokenValid(String token) {
87100
try {

src/main/java/com/example/library/user/CustomUserDetailsService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
import org.springframework.security.core.userdetails.UsernameNotFoundException;
66
import org.springframework.stereotype.Service;
77

8+
import lombok.RequiredArgsConstructor;
9+
10+
@RequiredArgsConstructor
811
@Service
912
public class CustomUserDetailsService implements UserDetailsService {
1013

1114
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CustomUserDetailsService.class);
1215

1316
private final UserRepository repository;
1417

15-
public CustomUserDetailsService(UserRepository userRepository) {
16-
this.repository = userRepository;
17-
}
18-
1918
@Override
2019
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
2120

src/main/java/com/example/library/user/UserLookupService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44

55
public interface UserLookupService {
66
Optional<User> findById(Long id);
7+
8+
Optional<User> findByEmail(String email);
79
}

src/main/java/com/example/library/user/UserLookupServiceImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,10 @@ public class UserLookupServiceImpl implements UserLookupService {
1818
public Optional<User> findById(Long id) {
1919
return repository.findById(id);
2020
}
21+
22+
@Override
23+
@Transactional(readOnly = true)
24+
public Optional<User> findByEmail(String email) {
25+
return repository.findByEmail(email);
26+
}
2127
}

0 commit comments

Comments
 (0)