Pular para o conteúdo principal

TP2 - API REST com JWT

Objetivos do TP​

Neste TP, você irá:

  1. ✅ Criar uma API REST segura
  2. ✅ Implementar autenticação JWT
  3. ✅ Gerenciar os endpoints login e register
  4. ✅ Proteger rotas por papel
  5. ✅ Implementar o refresh token

Duração estimada: 2h


Contexto​

Você está criando a API backend de um aplicativo mobile de gerenciamento de notas. A API deve ser stateless com autenticação JWT.


Parte 1: Setup do Projeto​

Dependências Maven​

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>

Configuração​

# application.properties
spring.datasource.url=jdbc:h2:mem:jwtdb
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop

# JWT Configuration
jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
jwt.expiration=3600000
jwt.refresh-expiration=604800000

Parte 2: JwtService​

@Service
@RequiredArgsConstructor
public class JwtService {

@Value("${jwt.secret}")
private String secretKey;

@Value("${jwt.expiration}")
private long jwtExpiration;

@Value("${jwt.refresh-expiration}")
private long refreshExpiration;

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}

public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, refreshExpiration);
}

private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey())
.compact();
}

public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSignInKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

private SecretKey getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}

Parte 3: JwtAuthenticationFilter​

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

final String authHeader = request.getHeader("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

try {
final String jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
// Token inválido - continuar sem autenticação
}

filterChain.doFilter(request, response);
}
}

Parte 4: SecurityConfig​

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.headers(headers -> headers.frameOptions(f -> f.disable()))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

Parte 5: DTOs​

// LoginRequest.java
@Data
public class LoginRequest {
@NotBlank(message = "Username obrigatório")
private String username;

@NotBlank(message = "Password obrigatória")
private String password;
}

// RegisterRequest.java
@Data
public class RegisterRequest {
@NotBlank @Size(min = 3, max = 50)
private String username;

@NotBlank @Email
private String email;

@NotBlank @Size(min = 6)
private String password;
}

// AuthResponse.java
@Data
@Builder
public class AuthResponse {
private String accessToken;
private String refreshToken;
private String username;
private List<String> roles;
}

// RefreshRequest.java
@Data
public class RefreshRequest {
@NotBlank
private String refreshToken;
}

Parte 6: AuthController​

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtService jwtService;
private final UserService userService;

@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
User user = userService.createUser(request.getUsername(), request.getEmail(), request.getPassword());
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());

return ResponseEntity.status(HttpStatus.CREATED).body(createAuthResponse(userDetails));
}

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);

UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
return ResponseEntity.ok(createAuthResponse(userDetails));
}

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@Valid @RequestBody RefreshRequest request) {
String username = jwtService.extractUsername(request.getRefreshToken());
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (jwtService.isTokenValid(request.getRefreshToken(), userDetails)) {
String accessToken = jwtService.generateToken(userDetails);
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(request.getRefreshToken())
.username(username)
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.build());
}

return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

private AuthResponse createAuthResponse(UserDetails userDetails) {
return AuthResponse.builder()
.accessToken(jwtService.generateToken(userDetails))
.refreshToken(jwtService.generateRefreshToken(userDetails))
.username(userDetails.getUsername())
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.build();
}
}

Parte 7: API Protegida​

NoteController​

@RestController
@RequestMapping("/api/notes")
@RequiredArgsConstructor
public class NoteController {

private final NoteRepository noteRepository;

@GetMapping
public List<Note> getMyNotes(@AuthenticationPrincipal UserDetails userDetails) {
return noteRepository.findByUsername(userDetails.getUsername());
}

@PostMapping
public Note createNote(@RequestBody Note note, @AuthenticationPrincipal UserDetails userDetails) {
note.setUsername(userDetails.getUsername());
return noteRepository.save(note);
}

@DeleteMapping("/{id}")
@PreAuthorize("@noteRepository.findById(#id).orElse(null)?.username == authentication.name or hasRole('ADMIN')")
public void deleteNote(@PathVariable Long id) {
noteRepository.deleteById(id);
}
}

Parte 8: Testes com cURL​

# 1. Cadastro
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@test.com","password":"password123"}'

# 2. Login
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"password123"}'

# Resposta: {"accessToken":"eyJ...","refreshToken":"eyJ...","username":"testuser","roles":["ROLE_USER"]}

# 3. Acesso protegido (substitua TOKEN pelo accessToken recebido)
curl http://localhost:8080/api/notes \
-H "Authorization: Bearer TOKEN"

# 4. Criar uma nota
curl -X POST http://localhost:8080/api/notes \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Minha nota","content":"Conteúdo da nota"}'

# 5. Refresh token
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"REFRESH_TOKEN"}'

Checklist​

  • POST /api/auth/register cria um usuário
  • POST /api/auth/login retorna os tokens
  • GET /api/notes sem token → 401
  • GET /api/notes com token → 200
  • Token expirado → 401
  • POST /api/auth/refresh → novo access token

🎯 Próxima Etapa​

➡️ TP8.3: Papéis e Permissões


⬅️ Voltar ao Módulo 8