From 55e4ef9da72f8b1ec897ee280de1dcf33af9246b Mon Sep 17 00:00:00 2001 From: sdikyarts Date: Sun, 26 May 2024 05:58:24 +0700 Subject: [PATCH] Add JWT Service --- build.gradle.kts | 5 ++ .../config/JWTAuthFilter.java | 48 +++++++++++++++++++ .../config/SecurityConfig.java | 29 +++++++++++ .../subscriptionadmin/utils/JWTUtils.java | 41 ++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 src/main/java/snackscription/subscriptionadmin/config/JWTAuthFilter.java create mode 100644 src/main/java/snackscription/subscriptionadmin/config/SecurityConfig.java create mode 100644 src/main/java/snackscription/subscriptionadmin/utils/JWTUtils.java diff --git a/build.gradle.kts b/build.gradle.kts index bec1fb7..5bd8d78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,6 +45,11 @@ dependencies { runtimeOnly("io.micrometer:micrometer-registry-prometheus:1.12.5") implementation("org.springframework.boot:spring-boot-starter-actuator:3.2.5") implementation("me.paulschwarz:spring-dotenv:4.0.0") + implementation("io.jsonwebtoken:jjwt-api:0.12.5") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-webflux") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5") } tasks.register("unitTest") { diff --git a/src/main/java/snackscription/subscriptionadmin/config/JWTAuthFilter.java b/src/main/java/snackscription/subscriptionadmin/config/JWTAuthFilter.java new file mode 100644 index 0000000..87e1be0 --- /dev/null +++ b/src/main/java/snackscription/subscriptionadmin/config/JWTAuthFilter.java @@ -0,0 +1,48 @@ +package snackscription.subscriptionadmin.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import snackscription.subscriptionadmin.utils.JWTUtils; + +import java.io.IOException; +import java.util.Collections; + +@Component +public class JWTAuthFilter extends OncePerRequestFilter { + + private final JWTUtils jwtUtils; + + public JWTAuthFilter(JWTUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwtToken; + + if (authHeader == null || authHeader.isBlank()) { + filterChain.doFilter(request, response); + return; + } + + jwtToken = authHeader.substring(7); + + if (jwtUtils.isTokenValid(jwtToken)) { + String role = jwtUtils.extractRole(jwtToken); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + null, null, Collections.singletonList(() -> role)); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/snackscription/subscriptionadmin/config/SecurityConfig.java b/src/main/java/snackscription/subscriptionadmin/config/SecurityConfig.java new file mode 100644 index 0000000..c4675d0 --- /dev/null +++ b/src/main/java/snackscription/subscriptionadmin/config/SecurityConfig.java @@ -0,0 +1,29 @@ +package snackscription.subscriptionadmin.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import snackscription.subscriptionadmin.utils.JWTUtils; + +public class SecurityConfig { + private final JWTUtils jwtUtils; + + public SecurityConfig(JWTUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests.requestMatchers("/adminSubscription/**", "/public/**").permitAll() + .anyRequest().authenticated()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(new JWTAuthFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class); + + return httpSecurity.build(); + } +} diff --git a/src/main/java/snackscription/subscriptionadmin/utils/JWTUtils.java b/src/main/java/snackscription/subscriptionadmin/utils/JWTUtils.java new file mode 100644 index 0000000..d6c2173 --- /dev/null +++ b/src/main/java/snackscription/subscriptionadmin/utils/JWTUtils.java @@ -0,0 +1,41 @@ +package snackscription.subscriptionadmin.utils; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Value; + +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import java.util.function.Function; + +@Component +public class JWTUtils { + private final SecretKey KEY; + + public JWTUtils(@Value("${JWT_SECRET}") String jwtSecret) { + byte[] keyBytes = Base64.getDecoder().decode(jwtSecret.getBytes(StandardCharsets.UTF_8)); + this.KEY = new SecretKeySpec(keyBytes, "HmacSHA256"); + } + + public String extractRole(String token) { + return extractClaims(token, claims -> claims.get("role", String.class)); + } + + private T extractClaims(String token, Function claimsTFunction){ + return claimsTFunction.apply(Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token).getPayload()); + } + + public boolean isTokenValid(String token) { + return !isTokenExpired(token); + } + + public boolean isTokenExpired(String token) { + return extractClaims(token, Claims::getExpiration).before(new Date()); + } +} \ No newline at end of file