Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public void commence(
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import static com.acon.server.global.auth.jwt.JwtValidationType.VALID_JWT;

import com.acon.server.global.auth.MemberAuthentication;
import com.acon.server.global.auth.jwt.JwtAuthenticationException;
import com.acon.server.global.auth.jwt.JwtTokenProvider;
import com.acon.server.global.exception.BusinessException;
import com.acon.server.global.auth.jwt.JwtValidationType;
import com.acon.server.global.exception.ErrorType;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -26,46 +27,69 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final CustomJwtAuthenticationEntryPoint authenticationEntryPoint;

// 각 HTTP 요청에 대해 토큰이 유효한지 확인하고, 유효하다면 해당 사용자를 인증 설정하는 필터링 로직
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
final String token = getJwtFromRequest(request);
final String token = resolveToken(request);

if (jwtTokenProvider.validateToken(token) == VALID_JWT) {
if (token == null) {
filterChain.doFilter(request, response);

return;
}

JwtValidationType result = jwtTokenProvider.validateToken(token);

if (result == VALID_JWT) {
Long memberId = jwtTokenProvider.getMemberIdFromJwt(token);

// authentication 객체 생성 -> principal에 유저정보를 담는다.
MemberAuthentication authentication = new MemberAuthentication(memberId.toString(), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);

filterChain.doFilter(request, response);

return;
}
} catch (Exception exception) {
// SecurityConfig에서 permitAll을 적용해도, Spring Security의 필터 체인을 거치므로
// 여기서 바로 Exception throw를 하게 되면 permitAll과 상관 없이 ExceptionTranslationFilter로 처리가 넘어간다.
// 따라서 예외를 직접 throw로 던져주는 것이 아닌, 발생시키기만 하고 다음 필터 호출로 이어지게끔 해야 하고, (doFilter)
// 이렇게 하면 API의 permitAll 적용 여부에 따라 ExceptionTranslationFilter를 거칠지 판단하게 된다.
log.error("JwtAuthentication Authentication Exception Occurs! - {}", exception.getMessage());

throw mapToAuthException(result);

} catch (JwtAuthenticationException ex) {
authenticationEntryPoint.commence(request, response, ex);
}
// 다음 필터로 요청 전달 (호출)
filterChain.doFilter(request, response);
}

// Authorization 헤더에서 JWT 토큰을 추출
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");

if (!StringUtils.hasText(bearerToken)) {
// throw new BusinessException(ErrorType.UN_LOGIN_ERROR);
} else if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring("Bearer ".length());
} else if (StringUtils.hasText(bearerToken) && !bearerToken.startsWith("Bearer ")) {
throw new BusinessException(ErrorType.BEARER_LOST_ERROR);
if (!StringUtils.hasText(bearer)) {
return null;
}

return null;
if (!bearer.startsWith("Bearer ")) {
throw new JwtAuthenticationException(ErrorType.BEARER_LOST_ERROR);
}

String token = bearer.substring("Bearer ".length()).trim();

if (!StringUtils.hasText(token)) {
throw new JwtAuthenticationException(ErrorType.INVALID_ACCESS_TOKEN_ERROR);
}

return token;
}

private JwtAuthenticationException mapToAuthException(JwtValidationType v) {
return switch (v) {
case EXPIRED_JWT_TOKEN -> new JwtAuthenticationException(ErrorType.EXPIRED_ACCESS_TOKEN_ERROR);
case INVALID_JWT_SIGNATURE, INVALID_JWT_TOKEN, UNSUPPORTED_JWT_TOKEN, EMPTY_JWT ->
new JwtAuthenticationException(ErrorType.INVALID_ACCESS_TOKEN_ERROR);
case VALID_JWT -> throw new IllegalStateException("VALID_JWT should not reach mapToAuthException");
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should be more descriptive. Consider adding context about the internal error: "Internal error: VALID_JWT token should not be mapped to an authentication exception"

Suggested change
case VALID_JWT -> throw new IllegalStateException("VALID_JWT should not reach mapToAuthException");
case VALID_JWT -> throw new IllegalStateException("Internal error: VALID_JWT token should not be mapped to an authentication exception. This indicates a logic error in the authentication flow. Please check the JWT validation and filter logic.");

Copilot uses AI. Check for mistakes.
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.acon.server.global.auth.jwt;

import com.acon.server.global.exception.ErrorType;
import lombok.Getter;
import org.springframework.security.core.AuthenticationException;

@Getter
public class JwtAuthenticationException extends AuthenticationException {

private final ErrorType errorType;

public JwtAuthenticationException(ErrorType errorType) {
super(errorType.getMessage());
this.errorType = errorType;
}

public JwtAuthenticationException(ErrorType errorType, Throwable cause) {
super(errorType.getMessage(), cause);
this.errorType = errorType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,24 @@ public class SecurityConfig {
private final CustomAccessDeniedHandler customAccessDeniedHandler;

private static final String[] AUTH_WHITE_LIST = {
"/api/v1/**",
"/api/v1/auth/**"
"/api/v1/auth/**",
"/api/v2/auth/**"
};

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> {
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exception -> {
exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint);
exception.accessDeniedHandler(customAccessDeniedHandler);
});

http.authorizeHttpRequests(auth -> {
auth.requestMatchers(AUTH_WHITE_LIST).permitAll();
auth.anyRequest().authenticated();
})
.authorizeHttpRequests(auth -> auth
.requestMatchers(AUTH_WHITE_LIST).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
Expand Down