Skip to content

Commit 7d13c26

Browse files
authored
Merge pull request #4 from nkcoder/refactor/jwt_auth_filter
[Refactor] JWT auth filter
2 parents f14e576 + cbfcb1d commit 7d13c26

6 files changed

Lines changed: 110 additions & 48 deletions

File tree

auto/clean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
3+
export SPRING_PROFILES_ACTIVE=test
4+
./gradlew clean

auto/test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
3+
export SPRING_PROFILES_ACTIVE=test
4+
./gradlew test

build.gradle.kts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ group = "org.nkcoder"
1717
version = "0.1.0"
1818

1919
java {
20-
sourceCompatibility = JavaVersion.VERSION_21
20+
toolchain {
21+
languageVersion.set(JavaLanguageVersion.of(21))
22+
}
2123
}
2224

2325
repositories {
@@ -174,6 +176,14 @@ sourceSets {
174176
}
175177
}
176178

179+
tasks.test {
180+
maxParallelForks = Runtime.getRuntime().availableProcessors()
181+
failFast = true
182+
183+
// Cache and incremental test execution
184+
outputs.cacheIf { true }
185+
}
186+
177187
// Test coverage: jacoco
178188
jacoco {
179189
toolVersion = "0.8.14"

gradle.properties

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# gradle.properties
2+
org.gradle.daemon=true
3+
org.gradle.parallel=true
4+
org.gradle.configureondemand=true
5+
org.gradle.caching=true
6+
# Configuration cache (ensure plugins/tasks support it)
7+
org.gradle.configuration-cache=true
8+
# Better memory for large projects
9+
org.gradle.jvmargs=-Xmx2g -XX:+UseG1GC -Dfile.encoding=UTF-8
10+
# Kotlin incremental compilation
11+
kotlin.incremental=true
12+
kotlin.compiler.execution.strategy=daemon

src/main/java/org/nkcoder/security/JwtAuthenticationEntryPoint.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package org.nkcoder.security;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4-
import jakarta.servlet.ServletException;
4+
import io.jsonwebtoken.ExpiredJwtException;
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
77
import java.io.IOException;
8+
import org.apache.logging.log4j.util.Strings;
89
import org.nkcoder.dto.common.ApiResponse;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
@@ -18,6 +19,8 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
1819

1920
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
2021

22+
private static final String CONTENT_TYPE_JSON = "application/json";
23+
2124
private final ObjectMapper objectMapper;
2225

2326
@Autowired
@@ -30,18 +33,30 @@ public void commence(
3033
HttpServletRequest request,
3134
HttpServletResponse response,
3235
AuthenticationException authException)
33-
throws IOException, ServletException {
36+
throws IOException {
3437

35-
logger.error("Unauthorized error: {}", authException.getMessage());
38+
logger.debug("Unauthorized access attempt to: {}", request.getRequestURI());
3639

37-
response.setContentType("application/json");
40+
response.setContentType(CONTENT_TYPE_JSON);
3841
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
3942

40-
ApiResponse<Object> apiResponse =
41-
ApiResponse.error("Unauthorized: " + authException.getMessage());
43+
String errorMessage = determineErrorMessage(authException);
44+
ApiResponse<Void> apiResponse = ApiResponse.error(errorMessage);
4245

4346
objectMapper.writeValue(response.getOutputStream(), apiResponse);
4447
response.getOutputStream().flush();
45-
response.getOutputStream().close();
48+
// Do NOT close the stream - let the servlet container manage it
49+
}
50+
51+
private String determineErrorMessage(AuthenticationException authException) {
52+
if (authException.getCause() instanceof ExpiredJwtException) {
53+
return "Token has expired";
54+
}
55+
56+
if (Strings.isNotBlank(authException.getMessage())) {
57+
return "Authentication required: " + authException.getMessage();
58+
}
59+
60+
return "Authentication required";
4661
}
4762
}
Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package org.nkcoder.security;
22

33
import io.jsonwebtoken.Claims;
4-
import io.jsonwebtoken.JwtException;
4+
import io.jsonwebtoken.ExpiredJwtException;
5+
import io.jsonwebtoken.MalformedJwtException;
6+
import io.jsonwebtoken.UnsupportedJwtException;
57
import jakarta.servlet.FilterChain;
68
import jakarta.servlet.ServletException;
79
import jakarta.servlet.http.HttpServletRequest;
810
import jakarta.servlet.http.HttpServletResponse;
911
import java.io.IOException;
1012
import java.util.List;
13+
import java.util.Optional;
1114
import java.util.UUID;
1215
import org.jetbrains.annotations.NotNull;
1316
import org.nkcoder.enums.Role;
@@ -29,6 +32,12 @@
2932
@Component
3033
public class JwtAuthenticationFilter extends OncePerRequestFilter {
3134

35+
private static final String AUTHORIZATION_HEADER = "Authorization";
36+
private static final String BEARER_PREFIX = "Bearer ";
37+
private static final String ATTRIBUTE_USER_ID = "userId";
38+
private static final String ATTRIBUTE_ROLE = "role";
39+
private static final String ATTRIBUTE_EMAIL = "email";
40+
3241
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
3342

3443
private final JwtUtil jwtUtil;
@@ -46,48 +55,56 @@ protected void doFilterInternal(
4655
throws ServletException, IOException {
4756
logger.debug("Processing authentication for request: {}", request.getRequestURI());
4857

49-
try {
50-
String jwt = getJwtFromRequest(request);
51-
52-
if (StringUtils.hasText(jwt) && !jwtUtil.isTokenExpired(jwt)) {
53-
Claims claims = jwtUtil.validateAccessToken(jwt);
54-
55-
UUID userId = UUID.fromString(claims.getSubject());
56-
String email = claims.get("email", String.class);
57-
String roleString = claims.get("role", String.class);
58-
Role role = Role.valueOf(roleString);
59-
60-
// Create authorities
61-
List<GrantedAuthority> authorities =
62-
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
63-
64-
UserDetails userDetails = new User(email, "", authorities);
65-
UsernamePasswordAuthenticationToken authentication =
66-
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
67-
68-
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
69-
70-
// Set custom attributes
71-
request.setAttribute("userId", userId);
72-
request.setAttribute("email", email);
73-
request.setAttribute("role", role);
74-
75-
SecurityContextHolder.getContext().setAuthentication(authentication);
76-
77-
logger.debug("Set authentication for user: {}", email);
78-
}
79-
} catch (JwtException e) {
80-
logger.error("Cannot set user authentication: {}", e.getMessage());
81-
}
58+
extractTokenFromRequest(request)
59+
.ifPresent(
60+
token -> {
61+
try {
62+
Claims claims = jwtUtil.validateAccessToken(token);
63+
64+
UUID userId = UUID.fromString(claims.getSubject());
65+
String email = claims.get("email", String.class);
66+
String roleString = claims.get("role", String.class);
67+
Role role = Role.valueOf(roleString);
68+
69+
// Create authorities
70+
List<GrantedAuthority> authorities =
71+
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
72+
73+
UserDetails userDetails = new User(email, "", authorities);
74+
UsernamePasswordAuthenticationToken authentication =
75+
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
76+
77+
authentication.setDetails(
78+
new WebAuthenticationDetailsSource().buildDetails(request));
79+
80+
// Set custom attributes
81+
request.setAttribute(ATTRIBUTE_USER_ID, userId);
82+
request.setAttribute(ATTRIBUTE_EMAIL, email);
83+
request.setAttribute(ATTRIBUTE_ROLE, role);
84+
85+
SecurityContextHolder.getContext().setAuthentication(authentication);
86+
87+
logger.debug("Set authentication for userId: {}", userId);
88+
} catch (ExpiredJwtException e) {
89+
logger.error("JWT token expired: {}", e.getMessage());
90+
} catch (MalformedJwtException e) {
91+
logger.error("Malformed JWT token: {}", e.getMessage());
92+
} catch (UnsupportedJwtException e) {
93+
logger.error("Unsupported JWT token: {}", e.getMessage());
94+
} catch (SecurityException e) {
95+
logger.error("JWT signature validation failed: {}", e.getMessage());
96+
} catch (IllegalArgumentException e) {
97+
logger.error("JWT token compact of handler are invalid: {}", e.getMessage());
98+
}
99+
});
82100

83101
filterChain.doFilter(request, response);
84102
}
85103

86-
private String getJwtFromRequest(HttpServletRequest request) {
87-
String bearerToken = request.getHeader("Authorization");
88-
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
89-
return bearerToken.substring(7);
90-
}
91-
return null;
104+
private Optional<String> extractTokenFromRequest(HttpServletRequest request) {
105+
return Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER))
106+
.filter(StringUtils::hasText)
107+
.filter(token -> token.startsWith(BEARER_PREFIX))
108+
.map(token -> token.substring(BEARER_PREFIX.length()));
92109
}
93110
}

0 commit comments

Comments
 (0)