Skip to main content

Spring Boot REST API JWT Authentication dengan PostgreSQL

· 16 min read
Ucup TopekoX
Ucup TopekoX
TimposuLabs creator

Pada materi ini kita akan membuat aplikasi REST API Authentication menggunakan:

  • Spring Boot
  • Spring Security
  • JSON Web Token (JWT)
  • PostgreSQL
  • Spring Data JPA
  • Maven
  • BCrypt
  • Jakarta Validation

Aplikasi yang akan dibuat memiliki fitur:

  • Registrasi pengguna
  • Login pengguna
  • Password hashing menggunakan BCrypt
  • Generate JWT
  • Validasi JWT
  • Authentication menggunakan Spring Security
  • Authorization berdasarkan role
  • Endpoint public
  • Endpoint yang membutuhkan authentication
  • Endpoint khusus administrator
  • PostgreSQL sebagai database
  • Global exception handler
  • Validasi request

1. Arsitektur Aplikasi

Secara sederhana, alur aplikasi yang akan kita buat adalah:

CLIENT
|
|
POST /api/auth/login
|
v
+-------------------+
| AuthController |
+-------------------+
|
v
+-------------------+
| AuthService |
+-------------------+
|
v
+-------------------+
| UserRepository |
+-------------------+
|
v
PostgreSQL
|
|
User ditemukan
|
v
Password Verification
|
v
JWT Service
|
v
Generate JWT
|
v
CLIENT
|
Bearer Token
|
v
JwtAuthenticationFilter
|
v
Validate JWT
|
v
SecurityContext
|
v
Controller

2. Apa Itu JWT?

JWT (JSON Web Token) adalah standar untuk membawa informasi atau klaim antar pihak dalam bentuk token yang dapat diverifikasi.

JWT biasanya digunakan untuk authentication pada REST API.

Contohnya setelah melakukan login:

POST /api/auth/login

Server memberikan response:

{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

Client kemudian menggunakan token tersebut ketika mengakses endpoint yang membutuhkan authentication.

Contohnya:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

3. Struktur JWT

JWT secara umum memiliki tiga bagian:

HEADER.PAYLOAD.SIGNATURE

Contoh:

eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJhZG1pbiJ9
.
signature

Ketiga bagian tersebut adalah:

  1. Header
  2. Payload
  3. Signature

4. Header JWT

Header biasanya berisi informasi algoritma dan tipe token.

Contohnya:

{
"alg": "HS256",
"typ": "JWT"
}

alg menunjukkan algoritma yang digunakan untuk menandatangani token.

Pada contoh ini digunakan:

HS256

5. Payload JWT

Payload berisi claims.

Contohnya:

{
"sub": "admin",
"iat": 1750000000,
"exp": 1750086400
}

Beberapa claim yang umum digunakan:

ClaimKeterangan
subSubject atau identitas pengguna
iatIssued At
expExpiration Time
issIssuer
audAudience

Jangan menyimpan informasi rahasia seperti password di dalam payload JWT.

JWT dapat didecode oleh client.


6. Signature JWT

Signature digunakan untuk memastikan token tidak dimodifikasi.

Secara sederhana:

Signature = Hash(
Header + Payload + Secret Key
)

Jika payload diubah, signature tidak akan cocok dengan signature yang dihasilkan server.


7. Teknologi yang Digunakan

Project ini menggunakan:

TeknologiFungsi
Java 21+Bahasa pemrograman
Spring BootFramework aplikasi
Spring WebMembuat REST API
Spring SecurityAuthentication dan Authorization
Spring Data JPADatabase access
PostgreSQLDatabase
JWTAuthentication token
BCryptPassword hashing
MavenDependency management
LombokMengurangi boilerplate code
ValidationValidasi request

8. Membuat Project Spring Boot

Project dapat dibuat menggunakan Spring Initializr.

Dependency yang diperlukan:

Spring Web
Spring Security
Spring Data JPA
PostgreSQL Driver
Validation
Lombok

Tambahkan dependency JWT secara manual pada pom.xml.


9. Struktur Project

Struktur project yang digunakan:

springboot-jwt-postgres

├── src
│ ├── main
│ │ ├── java
│ │ │ └── com.example.springjwt
│ │ │ │
│ │ │ ├── config
│ │ │ │ ├── ApplicationConfig.java
│ │ │ │ ├── JwtAuthenticationFilter.java
│ │ │ │ ├── JwtService.java
│ │ │ │ └── SecurityConfig.java
│ │ │ │
│ │ │ ├── controller
│ │ │ │ ├── AuthController.java
│ │ │ │ └── UserController.java
│ │ │ │
│ │ │ ├── dto
│ │ │ │ ├── LoginRequest.java
│ │ │ │ ├── RegisterRequest.java
│ │ │ │ ├── JwtResponse.java
│ │ │ │ └── UserResponse.java
│ │ │ │
│ │ │ ├── entity
│ │ │ │ ├── User.java
│ │ │ │ └── Role.java
│ │ │ │
│ │ │ ├── repository
│ │ │ │ └── UserRepository.java
│ │ │ │
│ │ │ ├── service
│ │ │ │ ├── AuthService.java
│ │ │ │ └── CustomUserDetailsService.java
│ │ │ │
│ │ │ └── SpringbootJwtApplication.java
│ │ │
│ │ └── resources
│ │ └── application.yml
│ │
│ └── test

└── pom.xml

10. Konfigurasi Database PostgreSQL

Buat database:

CREATE DATABASE springjwt;

Kemudian konfigurasi database pada:

src/main/resources/application.yml

Contoh:

spring:
datasource:
url: jdbc:postgresql://localhost:5432/springjwt
username: postgres
password: postgres

jpa:
hibernate:
ddl-auto: update
show-sql: true

jwt:
secret: 1234567890123456789012345678901234567890123456789012345678901234
expiration: 86400000

Untuk production, jangan menyimpan username, password database, dan JWT secret secara langsung di dalam source code.

Gunakan environment variable atau secret management.


11. Dependency JWT

Tambahkan dependency JWT pada pom.xml:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.7</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.7</version>
<scope>runtime</scope>
</dependency>

12. Entity User

Buat entity User:

package com.example.springjwt.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

@Column(unique = true, nullable = false)
private String username;

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
}

13. Entity Role

Buat enum:

package com.example.springjwt.entity;

public enum Role {

USER,
ADMIN
}

Role disimpan sebagai string pada database.

Contohnya:

USER
ADMIN

Hal ini lebih mudah dibaca dibandingkan menyimpan ordinal:

0
1

14. UserRepository

Buat repository:

package com.example.springjwt.repository;

import com.example.springjwt.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);

boolean existsByUsername(String username);

boolean existsByEmail(String email);
}

Repository digunakan untuk berkomunikasi dengan database melalui Spring Data JPA.


15. DTO RegisterRequest

Jangan langsung menggunakan entity sebagai request API.

Buat DTO:

package com.example.springjwt.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class RegisterRequest {

@NotBlank
private String name;

@NotBlank
@Size(min = 4, max = 50)
private String username;

@NotBlank
@Email
private String email;

@NotBlank
@Size(min = 8, max = 100)
private String password;
}

DTO membantu memisahkan struktur request API dari entity database.


16. DTO LoginRequest

Buat:

package com.example.springjwt.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class LoginRequest {

@NotBlank
private String username;

@NotBlank
private String password;
}

17. DTO JwtResponse

Buat:

package com.example.springjwt.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class JwtResponse {

private String token;
}

Response login akan menghasilkan:

{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

18. DTO UserResponse

Buat DTO untuk response user:

package com.example.springjwt.dto;

import com.example.springjwt.entity.Role;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class UserResponse {

private Long id;
private String name;
private String username;
private String email;
private Role role;
}

Password tidak dimasukkan ke dalam response.


19. CustomUserDetailsService

Spring Security membutuhkan UserDetailsService untuk mengambil informasi user dari database.

Buat:

package com.example.springjwt.service;

import com.example.springjwt.entity.User;
import com.example.springjwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {

User user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException(
"User tidak ditemukan"
)
);

return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
List.of(
new SimpleGrantedAuthority(
"ROLE_" + user.getRole().name()
)
)
);
}
}

20. Password Encoder

Password tidak boleh disimpan dalam bentuk plaintext.

Contoh yang salah:

admin123

Password harus di-hash menggunakan BCrypt.

Contohnya:

$2a$10$....

Buat bean:

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

21. ApplicationConfig

Buat file:

ApplicationConfig.java

Contoh:

package com.example.springjwt.config;

import com.example.springjwt.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {

private final CustomUserDetailsService userDetailsService;

@Bean
public UserDetailsService userDetailsService() {
return userDetailsService;
}

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

@Bean
public DaoAuthenticationProvider authenticationProvider() {

DaoAuthenticationProvider provider =
new DaoAuthenticationProvider();

provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());

return provider;
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration
) throws Exception {

return configuration.getAuthenticationManager();
}
}

22. JwtService

JwtService bertanggung jawab untuk membuat dan memvalidasi JWT.

package com.example.springjwt.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {

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

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

private SecretKey getSigningKey() {

return Keys.hmacShaKeyFor(
secret.getBytes(StandardCharsets.UTF_8)
);
}

public String generateToken(UserDetails userDetails) {

Map<String, Object> claims = new HashMap<>();

return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(
new Date(
System.currentTimeMillis()
+ expiration
)
)
.signWith(getSigningKey())
.compact();
}

public String extractUsername(String token) {

return extractClaim(
token,
Claims::getSubject
);
}

public <T> T extractClaim(
String token,
Function<Claims, T> resolver
) {

Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();

return resolver.apply(claims);
}

public boolean isTokenValid(
String token,
UserDetails userDetails
) {

String username = extractUsername(token);

return username.equals(userDetails.getUsername())
&& !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {

return extractClaim(
token,
Claims::getExpiration
).before(new Date());
}
}

23. JWT Authentication Filter

Filter bertugas mengambil token dari request.

Token biasanya dikirim melalui:

Authorization: Bearer <token>

Buat:

package com.example.springjwt.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@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;
}

final String jwt =
authHeader.substring(7);

final String username;

try {

username = jwtService.extractUsername(jwt);

} catch (Exception e) {

filterChain.doFilter(request, response);
return;
}

if (username != null &&
SecurityContextHolder
.getContext()
.getAuthentication() == null) {

UserDetails userDetails =
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);
}
}

filterChain.doFilter(request, response);
}
}

24. SecurityConfig

Konfigurasi Spring Security:

package com.example.springjwt.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationProvider authenticationProvider;

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {

http
.csrf(csrf -> csrf.disable())

.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)

.authenticationProvider(
authenticationProvider
)

.authorizeHttpRequests(auth -> auth

.requestMatchers(
"/api/auth/**"
).permitAll()

.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**"
).permitAll()

.anyRequest()
.authenticated()
)

.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);

return http.build();
}
}

25. Mengapa Session Stateless?

REST API menggunakan JWT biasanya dibuat stateless.

Artinya server tidak menyimpan session login seperti aplikasi berbasis session tradisional.

Client membawa token pada setiap request.

Contohnya:

GET /api/users/profile
Authorization: Bearer eyJhbGciOi...

Server memvalidasi token tersebut.


26. AuthService

Service untuk register dan login:

package com.example.springjwt.service;

import com.example.springjwt.config.JwtService;
import com.example.springjwt.dto.JwtResponse;
import com.example.springjwt.dto.LoginRequest;
import com.example.springjwt.dto.RegisterRequest;
import com.example.springjwt.entity.Role;
import com.example.springjwt.entity.User;
import com.example.springjwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final CustomUserDetailsService userDetailsService;
private final JwtService jwtService;

public void register(RegisterRequest request) {

if (userRepository.existsByUsername(
request.getUsername()
)) {
throw new RuntimeException(
"Username sudah digunakan"
);
}

if (userRepository.existsByEmail(
request.getEmail()
)) {
throw new RuntimeException(
"Email sudah digunakan"
);
}

User user = User.builder()
.name(request.getName())
.username(request.getUsername())
.email(request.getEmail())
.password(
passwordEncoder.encode(
request.getPassword()
)
)
.role(Role.USER)
.build();

userRepository.save(user);
}

public JwtResponse login(
LoginRequest request
) {

authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);

UserDetails userDetails =
userDetailsService.loadUserByUsername(
request.getUsername()
);

String token =
jwtService.generateToken(userDetails);

return new JwtResponse(token);
}
}

27. AuthController

Buat controller:

package com.example.springjwt.controller;

import com.example.springjwt.dto.JwtResponse;
import com.example.springjwt.dto.LoginRequest;
import com.example.springjwt.dto.RegisterRequest;
import com.example.springjwt.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

private final AuthService authService;

@PostMapping("/register")
public ResponseEntity<?> register(
@Valid @RequestBody RegisterRequest request
) {

authService.register(request);

return ResponseEntity.ok(
"Register berhasil"
);
}

@PostMapping("/login")
public ResponseEntity<JwtResponse> login(
@Valid @RequestBody LoginRequest request
) {

return ResponseEntity.ok(
authService.login(request)
);
}
}

28. Endpoint Register

Endpoint:

POST /api/auth/register

Request:

{
"name": "Administrator",
"username": "admin",
"email": "admin@gmail.com",
"password": "admin12345"
}

Response:

Register berhasil

Database akan menyimpan password dalam bentuk hash BCrypt.

Contohnya:

$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Bukan:

admin12345

29. Endpoint Login

Endpoint:

POST /api/auth/login

Request:

{
"username": "admin",
"password": "admin12345"
}

Server akan:

Username
|
v
Database
|
v
User ditemukan
|
v
Password Verification
|
v
AuthenticationManager
|
v
Generate JWT
|
v
Response

Response:

{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

30. UserController

Buat controller untuk endpoint user:

package com.example.springjwt.controller;

import com.example.springjwt.dto.UserResponse;
import com.example.springjwt.entity.User;
import com.example.springjwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

private final UserRepository userRepository;

@GetMapping("/profile")
public UserResponse profile(
Authentication authentication
) {

User user = userRepository
.findByUsername(
authentication.getName()
)
.orElseThrow();

return UserResponse.builder()
.id(user.getId())
.name(user.getName())
.username(user.getUsername())
.email(user.getEmail())
.role(user.getRole())
.build();
}

@GetMapping
public List<UserResponse> users() {

return userRepository.findAll()
.stream()
.map(user ->
UserResponse.builder()
.id(user.getId())
.name(user.getName())
.username(user.getUsername())
.email(user.getEmail())
.role(user.getRole())
.build()
)
.toList();
}
}

31. Endpoint Profile

Endpoint:

GET /api/users/profile

Endpoint ini membutuhkan JWT.

Request:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Response:

{
"id": 1,
"name": "Administrator",
"username": "admin",
"email": "admin@gmail.com",
"role": "USER"
}

Password tidak pernah dikembalikan.


32. Authorization Berdasarkan Role

Authentication dan authorization adalah dua hal yang berbeda.

Authentication

Menjawab:

Siapa pengguna ini?

Contohnya:

admin

Authorization

Menjawab:

Apa yang boleh dilakukan pengguna ini?

Contohnya:

ADMIN
USER

33. Endpoint Khusus ADMIN

Kita dapat menggunakan:

@PreAuthorize("hasRole('ADMIN')")

Contohnya:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String admin() {

return "Halaman administrator";
}

User biasa tidak dapat mengakses endpoint tersebut.


34. Mengaktifkan Method Security

Pada SecurityConfig:

@EnableMethodSecurity

Contoh lengkap:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}

Dengan demikian annotation seperti:

@PreAuthorize("hasRole('ADMIN')")

dapat digunakan.


35. Role ADMIN

Saat register, kita sebaiknya tidak membiarkan client menentukan role.

Contoh yang tidak disarankan:

{
"name": "User",
"username": "user",
"email": "user@gmail.com",
"password": "password123",
"role": "ADMIN"
}

Hal ini berbahaya karena user dapat membuat dirinya sendiri menjadi administrator.

Lebih aman menentukan role secara server-side:

.role(Role.USER)

Jika ingin membuat administrator, lakukan melalui proses administratif khusus.


36. Testing dengan Postman

Urutan testing:

1. Register
2. Login
3. Copy JWT
4. Akses profile
5. Akses endpoint protected
6. Test role authorization

37. Test Register

Request:

POST http://localhost:8080/api/auth/register

Header:

Content-Type: application/json

Body:

{
"name": "Ucup",
"username": "ucup",
"email": "ucup@gmail.com",
"password": "password123"
}

38. Test Login

Request:

POST http://localhost:8080/api/auth/login

Body:

{
"username": "ucup",
"password": "password123"
}

Response:

{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

Copy nilai token.


39. Test Profile

Request:

GET http://localhost:8080/api/users/profile

Tambahkan header:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Jika token valid, server memberikan response user.


40. Apa yang Terjadi Ketika Token Tidak Dikirim?

Misalnya:

GET /api/users/profile

tanpa:

Authorization: Bearer <token>

Maka request tidak memiliki authentication.

Spring Security akan menolak request tersebut.


41. Apa yang Terjadi Jika Token Expired?

JWT memiliki expiration time.

Contohnya:

jwt:
expiration: 86400000

Nilai tersebut adalah:

86.400.000 ms

atau:

24 jam

Setelah token expired, client harus melakukan authentication kembali atau menggunakan mekanisme refresh token jika sistem menggunakan refresh token.


42. Access Token dan Refresh Token

Untuk aplikasi yang lebih serius, authentication dapat menggunakan dua jenis token:

Access Token
|
|-- Umur pendek
|
+----> digunakan untuk API


Refresh Token
|
|-- Umur lebih panjang
|
+----> digunakan mendapatkan Access Token baru

Contohnya:

Access Token = 15 menit
Refresh Token = 7 hari

Pendekatan ini lebih baik daripada menggunakan access token yang memiliki masa berlaku sangat panjang.


43. Keamanan Password

Password tidak boleh disimpan seperti:

password123

Gunakan:

passwordEncoder.encode(password)

Saat login, Spring Security akan membandingkan password menggunakan:

passwordEncoder.matches(
rawPassword,
encodedPassword
)

44. Hashing Bukan Encryption

Penting memahami perbedaannya.

Hashing

Hashing bersifat satu arah.

Password
|
v
Hash

Hash tidak seharusnya dapat dikembalikan menjadi password asli.

Contoh:

BCrypt
Argon2
PBKDF2

Encryption

Encryption bersifat dua arah.

Plaintext
|
v
Encryption
|
v
Ciphertext
|
v
Decryption
|
v
Plaintext

Untuk penyimpanan password, gunakan password hashing seperti BCrypt atau Argon2, bukan encryption biasa.


45. Jangan Menyimpan JWT Secret di Git

Contoh yang tidak baik:

jwt:
secret: my-secret-production-123456

Kemudian file tersebut di-upload ke GitHub.

Lebih baik:

jwt:
secret: ${JWT_SECRET}

Kemudian environment variable:

JWT_SECRET=your-long-random-secret

Untuk production, gunakan secret manager atau mekanisme secret management yang sesuai dengan infrastructure yang digunakan.


46. Endpoint API

Setelah selesai, aplikasi memiliki endpoint:

MethodEndpointAuthenticationKeterangan
POST/api/auth/registerTidakRegistrasi
POST/api/auth/loginTidakLogin
GET/api/users/profileYaProfile
GET/api/usersYaDaftar user
GET/api/users/adminADMINEndpoint administrator

47. Alur Register

Client
|
| POST /api/auth/register
|
v
AuthController
|
v
AuthService
|
+--> Validasi username
|
+--> Validasi email
|
+--> Hash password
|
v
UserRepository
|
v
PostgreSQL

48. Alur Login

Client
|
| username + password
v
AuthController
|
v
AuthService
|
v
AuthenticationManager
|
v
UserDetailsService
|
v
PostgreSQL
|
v
Password Verification
|
v
JwtService
|
v
JWT
|
v
Client

49. Alur Request dengan JWT

Client
|
| Authorization: Bearer JWT
v
JwtAuthenticationFilter
|
v
Extract JWT
|
v
Validate Signature
|
v
Check Expiration
|
v
Load User
|
v
SecurityContext
|
v
Controller

50. Stateless Authentication

Dengan JWT, server tidak perlu menyimpan session authentication untuk setiap client.

Contohnya:

Client A
|
+---- JWT A
|
v
API Server


Client B
|
+---- JWT B
|
v
API Server

Setiap request membawa credential dalam bentuk token.


51. Keuntungan JWT

Beberapa keuntungan penggunaan JWT:

  • Cocok untuk REST API
  • Stateless
  • Mudah digunakan oleh frontend
  • Cocok untuk mobile application
  • Dapat digunakan lintas service
  • Tidak membutuhkan session server tradisional
  • Mudah diintegrasikan dengan Spring Security

52. Kekurangan JWT

JWT juga memiliki beberapa kekurangan.

Token sulit dicabut

Jika JWT masih valid, token dapat digunakan sampai expiration.

Karena itu sistem production sering menggunakan:

Short-lived Access Token
+
Refresh Token

Ukuran token

JWT dapat lebih besar dibanding session ID.

Secret harus dijaga

Jika secret key bocor, attacker dapat membuat token yang valid untuk algoritma symmetric signing yang digunakan.


53. JWT Tidak Sama dengan Encryption

Hal yang sangat penting:

JWT != Encryption

Payload JWT umumnya dapat dibaca oleh pihak yang memiliki token.

Jadi jangan menyimpan:

{
"password": "password123"
}

di dalam JWT.

JWT sebaiknya hanya menyimpan informasi yang diperlukan.

Contohnya:

{
"sub": "ucup",
"role": "USER",
"iat": 1750000000,
"exp": 1750086400
}

54. Error Handling

Aplikasi REST API sebaiknya memiliki response error yang konsisten.

Contohnya:

{
"status": 400,
"message": "Username sudah digunakan",
"timestamp": "2026-08-16T19:00:00"
}

Daripada mengembalikan:

java.lang.RuntimeException

secara langsung.


55. Global Exception Handler

Buat:

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> handleRuntimeException(
RuntimeException exception
) {

return ResponseEntity
.badRequest()
.body(
Map.of(
"message",
exception.getMessage()
)
);
}
}

Untuk aplikasi production, exception handler sebaiknya dibuat lebih spesifik untuk masing-masing kategori error.


56. Validasi Request

Gunakan:

@Valid

Contohnya:

@PostMapping("/register")
public ResponseEntity<?> register(
@Valid @RequestBody RegisterRequest request
) {
...
}

Kemudian DTO:

@NotBlank
private String username;

dan:

@Size(min = 8)
private String password;

Dengan demikian input yang tidak valid dapat ditolak sebelum diproses lebih lanjut.


57. Pengujian dengan cURL

Register:

curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "Ucup",
"username": "ucup",
"email": "ucup@gmail.com",
"password": "password123"
}'

Login:

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

Profile:

curl http://localhost:8080/api/users/profile \
-H "Authorization: Bearer YOUR_TOKEN"

58. Pengembangan dengan Docker

Aplikasi ini juga dapat dikembangkan menggunakan Docker.

Arsitekturnya:

Docker Compose
|
+-----------+-----------+
| |
v v
Spring Boot API PostgreSQL
Container Container
| |
+-----------+-----------+
|
Network

Contoh service:

services:

app:
build: .
ports:
- "8080:8080"
depends_on:
- postgres

postgres:
image: postgres:17
environment:
POSTGRES_DB: springjwt
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

59. Rekomendasi untuk Production

Untuk aplikasi production, beberapa hal perlu diperhatikan.

Gunakan HTTPS

Jangan mengirim JWT melalui koneksi HTTP biasa.

Gunakan:

HTTPS

sehingga token tidak mudah disadap ketika berada dalam perjalanan.

Gunakan Secret yang kuat

Jangan menggunakan:

secret123

Gunakan random secret dengan panjang yang memadai.

Jangan menyimpan password plaintext

Selalu gunakan password hashing.

Jangan menyimpan JWT secret di repository

Gunakan environment variable atau secret manager.

Gunakan Access Token dengan lifetime pendek

Contohnya:

15 menit

dan gunakan refresh token jika diperlukan.

Jangan memasukkan data sensitif ke JWT

JWT bukan tempat untuk menyimpan password atau data rahasia.


60. Arsitektur Akhir

Secara keseluruhan aplikasi dapat digambarkan:

CLIENT
|
|
REST API Request
|
v
+----------------------+
| Spring Security |
+----------------------+
|
v
+----------------------+
| JWT Authentication |
| Filter |
+----------------------+
|
v
+----------------------+
| SecurityContext |
+----------------------+
|
v
+----------------------+
| Controller |
+----------------------+
|
v
+----------------------+
| Service |
+----------------------+
|
v
+----------------------+
| Repository |
+----------------------+
|
v
+----------------------+
| PostgreSQL |
+----------------------+

61. Ringkasan

Pada materi ini kita telah membuat REST API menggunakan Spring Boot dengan authentication berbasis JWT.

Komponen utama yang digunakan:

Spring Boot
|
+-- Spring Web
|
+-- Spring Security
|
+-- Spring Data JPA
|
+-- JWT
|
+-- BCrypt
|
+-- PostgreSQL

Alur authentication:

REGISTER
|
v
Password
|
v
BCrypt Hash
|
v
PostgreSQL

Kemudian:

LOGIN
|
v
Username + Password
|
v
AuthenticationManager
|
v
Password Verification
|
v
JWT Generation
|
v
Client

Ketika mengakses API:

Client
|
| Bearer JWT
v
JWT Filter
|
v
Validate JWT
|
v
SecurityContext
|
v
Protected API

62. Pengembangan Selanjutnya

Setelah memahami authentication JWT dasar, aplikasi dapat dikembangkan menjadi sistem yang lebih lengkap dengan fitur:

  • Refresh Token
  • Logout dan token revocation
  • Role-Based Access Control
  • Permission-Based Access Control
  • Swagger/OpenAPI
  • Flyway
  • Database migration
  • Audit log
  • Pagination
  • Sorting
  • Filtering
  • Global exception handling
  • Unit testing
  • Integration testing
  • Testcontainers
  • Docker
  • Docker Compose
  • HTTPS
  • Rate limiting
  • CORS configuration
  • Security headers
  • OAuth2
  • OpenID Connect

Dengan mempelajari tahapan tersebut, kita tidak hanya memahami cara membuat login menggunakan JWT, tetapi juga memahami bagaimana membangun REST API Spring Boot yang memiliki authentication, authorization, database persistence, dan security architecture yang lebih siap untuk dikembangkan ke lingkungan production.