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.