Tambahkan dependensi Maven di bawah ini ke proyek Spring Boot Anda:
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-web
com.mysql
mysql-connector-j
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-impl
0.12.3
runtime
io.jsonwebtoken
jjwt-api
0.12.3
io.jsonwebtoken
jjwt-jackson
0.12.3
runtime
Karena kita menggunakan MySQL sebagai database, kita perlu mengkonfigurasi URL database, nama pengguna, dan kata sandi agar Spring dapat membuat koneksi dengan database saat startup. Buka file src/main/resources/application.properties dan tambahkan properti berikut ke dalamnya:
spring.application.name=springrestjwt
# setup database connection
# driver database
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# database url connection
spring.datasource.url=jdbc:mysql://localhost:3306/demo
# database username
spring.datasource.username=root
# database password
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format-sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# JWT Secret Key
jwt.secret=your-very-secure-secret-key-change-this-for-production
# 24h=86400000ms in 1m=60000ms
jwt.expiration=60000
Pada langkah ini, kita akan membuat entitas JPA User dan Role menggunakan manyToOne relationship.
package com.ombagoes.springrestjwt.user;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.ombagoes.springrestjwt.role.Role;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
@NotBlank(message = "Name is mandatory")
@Pattern(regexp = "[a-zA-Z\\s]+$", message = "invalid name, char only")
private String name;
@Column(nullable = false, unique = true)
@NotBlank(message = "Email is mandatory")
@Email
private String email;
@Column(nullable = false)
@NotBlank(message = "Password must fill.")
@Pattern(regexp = "^(?=.*\\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\\w\\d\\s:])([^\\s]){8,}$", message = "Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character")
private String password;
@Column(nullable = false, columnDefinition = "boolean default true")
private boolean enabled;
@JsonIgnoreProperties("users")//avoid looping
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "role_id")
private Role role;
@Column(name = "created_at")
private Date createdAt;
@PrePersist
public void addTimestamp() {
createdAt = new Date();
}
@Column(name = "updated_at")
private Date updatedAt;
@PreUpdate
public void updateTimestamp() {
updatedAt = new Date();
}
}
package com.ombagoes.springrestjwt.role;
import com.ombagoes.springrestjwt.user.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List users = new ArrayList<>();
}
Selanjutnya, dalam paket repositori, kita membuat interface UserRepository dan RoleRepository.
package com.ombagoes.thirdJwt.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
Optional findByEmail(String email);
}
package com.ombagoes.thirdJwt.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List allUsers() {
return new ArrayList<>(userRepository.findAll());
}
}
package com.ombagoes.thirdJwt.role;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RoleRepository extends JpaRepository {
}
Role tidak menggunakan service karena tidak ada kebutuhan api untuk menampilkan data.
Controler nantinya akan berfungsi untuk menampilkan data user dan profile setelah login.
package com.ombagoes.springrestjwt.user;
import com.ombagoes.springrestjwt.auth.CustomUserDetailsService;
import com.ombagoes.springrestjwt.user.dtos.UpdateProfileDto;
import com.ombagoes.springrestjwt.util.JwtUtil;
import com.ombagoes.springrestjwt.util.ValidationUtil;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@Slf4j
public class UserController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Autowired
private ValidationUtil validationUtil;
@GetMapping("/users")
public List allUsers() {
return userService.allUsers();
}
@GetMapping("/profile")
public UserDetails getProfile() {
Authentication authenticationToken = SecurityContextHolder.getContext().getAuthentication();
String username= authenticationToken.getName();
return customUserDetailsService.loadUserByUsername(username);
}
@GetMapping("/me")
public ResponseEntity
Kelas ini akan digunakan untuk membaca token jwt dari header yang akam divalidasi oleh Security Config.
package com.ombagoes.springrestjwt.filter;
import com.ombagoes.springrestjwt.auth.CustomUserDetailsService;
import com.ombagoes.springrestjwt.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import java.io.IOException;
@Slf4j
@Component // Add this annotation
public class JwtRequestFilter extends OncePerRequestFilter {
private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
@Autowired
public JwtRequestFilter(JwtUtil jwtUtil, CustomUserDetailsService customUserDetailsService,HandlerExceptionResolver handlerExceptionResolver) {
this.jwtUtil = jwtUtil;
this.customUserDetailsService = customUserDetailsService;
this.handlerExceptionResolver = handlerExceptionResolver;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain) throws ServletException, IOException {
log.info("doFilterInternal(-)");
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
try {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
log.info("read bearer(-)");
jwtToken = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwtToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwtToken, username)) {
log.info("validateToken(-)");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}else{
throw new AccessDeniedException(null);
}
}
chain.doFilter(request, response);
}catch (Exception e){
handlerExceptionResolver.resolveException(request, response, null, e);
}
}
}
Kelas berikutnya digunakan untuk mengclaim jwt setelah login berhasil.
package com.ombagoes.thirdJwt.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@Slf4j
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expiration;
public String generateToken(Long id, String username) {
log.info("generateToken(-)");
return Jwts.builder()
.setId(id.toString())
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Claims extractClaims(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
public boolean isTokenExpired(String token) {
return extractClaims(token).getExpiration().before(new Date());
}
public boolean validateToken(String token, String username) {
return (username.equals(extractUsername(token)) && !isTokenExpired(token));
}
}
package com.ombagoes.thirdJwt.config;
import com.ombagoes.thirdJwt.filter.JwtRequestFilter;
import com.ombagoes.thirdJwt.auth.CustomUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
private final CustomUserDetailsService customUserDetailsService;
public SecurityConfig(JwtRequestFilter jwtRequestFilter, CustomUserDetailsService customUserDetailsService) {
this.jwtRequestFilter = jwtRequestFilter;
this.customUserDetailsService = customUserDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
log.info("securityFilterChain(-)");
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register","/login").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(authProvider);
}
@Bean
public UserDetailsService userDetailsService() {
return customUserDetailsService;
}
}
Kita menggunakan dua anotasi : “@Configuration” dan “@EnableWebSecurity”. Anotasi ini memberi tahu Spring Security untuk menggunakan konfigurasi keamanan custom, bukan bawaan.
Pada baris yang ditandai, mengatur route /login dan /register untuk tidak di protect.
package com.ombagoes.thirdJwt.auth;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String username;
private String password;
private boolean enabled;
}
Dalam konteks REST API, service merujuk pada bagian dari aplikasi atau sistem yang menyediakan fungsionalitas tertentu melalui protokol HTTP. Dengan kata lain, service adalah implementasi dari operasi atau fungsi yang dapat diakses melalui endpoint API.
package com.ombagoes.springrestjwt.auth;
import com.ombagoes.springrestjwt.role.RoleRepository;
import com.ombagoes.springrestjwt.user.User;
import com.ombagoes.springrestjwt.user.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@Slf4j
public class AuthenticationService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
public String signup(User input) {
input.setPassword(passwordEncoder.encode(input.getPassword()));
Optional user = userRepository.findByEmail(input.getEmail());
if (user.isPresent()){
return "Username exists";
}
try {
userRepository.save(input);
}
catch (Exception e) {
log.info(e.getMessage());
return "Signup Failed.";
}
return "";
}
public Long authenticate(AuthenticationRequest input) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
input.getUsername(),
input.getPassword()
)
);
return userRepository.findByEmail(input.getUsername()).orElseThrow().getId();
}catch (BadCredentialsException e){
log.info("Invalid username or password");
}catch (Exception e){
log.info(e.getMessage());
}
return 0L;
}
}
package com.ombagoes.springrestjwt.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.List;
@Component
@Slf4j
public class ValidationUtil {
public String doValidation(BindingResult bindingResult) {
StringBuilder bindErrorBuilder = new StringBuilder();
List errors = bindingResult.getFieldErrors();
for (FieldError error : errors ) {
bindErrorBuilder.append(error.getField()).append(" : ").append(error.getDefaultMessage()).append(", ");
}
if(!bindErrorBuilder.isEmpty()) {
return bindErrorBuilder.substring(0,bindErrorBuilder.length() - 2);
}
return "";
}
}
package com.ombagoes.springrestjwt.auth;
import com.ombagoes.springrestjwt.user.User;
import com.ombagoes.springrestjwt.user.UserRepository;
import com.ombagoes.springrestjwt.util.JwtUtil;
import com.ombagoes.springrestjwt.util.ValidationUtil;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("auth")
@CrossOrigin("*")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private AuthenticationService authenticationService;
@Autowired
private UserRepository userRepository;
@Autowired
private ValidationUtil validationUtil;
@PostMapping("/register")
public ResponseEntity
./mvnw spring-boot:run
Forbiidden tidak ada return apapun. Untuk menampilkan error(jika anda mau). Buka Class JwtFilter lalu tambahkan Try Catch sbb :
try {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
log.info("read bearer(-)");
jwtToken = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwtToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwtToken, username)) {
log.info("validateToken(-)");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
chain.doFilter(request, response);
}catch (Exception e){
handlerExceptionResolver.resolveException(request, response, null, e);
}
Lalu buat Class GlobalExceptionHandler sbb :
package com.ombagoes.springrestjwt.exceptions;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ProblemDetail handleSecurityException(Exception exception) {
ProblemDetail errorDetail = null;
// TODO send this stack trace to an observability tool
// exception.printStackTrace();
if (exception instanceof BadCredentialsException) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(401), exception.getMessage());
errorDetail.setProperty("description", "The username or password is incorrect");
return errorDetail;
}
if (exception instanceof AccountStatusException) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage());
errorDetail.setProperty("description", "The account is locked");
}
if (exception instanceof AccessDeniedException) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage());
errorDetail.setProperty("description", "You are not authorized to access this resource");
}
if (exception instanceof ExpiredJwtException) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage());
errorDetail.setProperty("description", "The JWT token has expired");
}
if (errorDetail == null) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(500), exception.getMessage());
errorDetail.setProperty("description", "Unknown internal server error.");
}
return errorDetail;
}
}
Hasilnya sbb :