Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT]스프링시큐리티 에러처리 #39

Merged
merged 3 commits into from
Jan 16, 2025
Merged
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
@@ -34,8 +34,8 @@ public enum ErrorCode {
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT4001", "유효하지 않은 JWT 토큰입니다."),
INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED, "JWT4002", "JWT 서명이 유효하지 않습니다."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "JWT4003", "JWT 토큰이 만료되었습니다."),
UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "JWT4004", "지원하지 않는 JWT 토큰입니다."),
EMPTY_CLAIMS(HttpStatus.UNAUTHORIZED, "JWT4005", "JWT claims 문자열이 비어 있습니다.");
MISSING_TOKEN(HttpStatus.UNAUTHORIZED, "JWT4000", "JWT 토큰이 요청에 포함되어 있지 않습니다.");


private final HttpStatus httpStatus;
private final String code;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.appsolve.wearther_backend.apiResponse.exception.handler;

import com.appsolve.wearther_backend.apiResponse.ApiResponse;
import com.appsolve.wearther_backend.apiResponse.exception.CustomException;
import com.appsolve.wearther_backend.apiResponse.exception.ErrorCode;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
//권한이 없는 경우 동작함
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ErrorCode errorCode = ErrorCode._FORBIDDEN;

CustomException customException = new CustomException(errorCode);

ApiResponse<Object> apiResponse = ApiResponse.fail(customException, null);

response.setStatus(errorCode.getHttpStatus().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.appsolve.wearther_backend.apiResponse.exception.handler;

import com.appsolve.wearther_backend.apiResponse.ApiResponse;
import com.appsolve.wearther_backend.apiResponse.exception.CustomException;
import com.appsolve.wearther_backend.apiResponse.exception.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;

//로그인 하지 않은 사용자에게서 작동
@Slf4j
@Component
public class CustomAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
log.info("[CustomAuthenticationEntryPointHandler] :: {}", authException.getMessage());
log.info("[CustomAuthenticationEntryPointHandler] :: {}", request.getRequestURL());

ErrorCode errorCode = ErrorCode.INVALID_TOKEN;
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || authorizationHeader.trim().isEmpty()) {
errorCode = ErrorCode.MISSING_TOKEN;
}

CustomException customException = new CustomException(errorCode);
ApiResponse<Object> apiResponse = ApiResponse.fail(customException, null);

response.setStatus(errorCode.getHttpStatus().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

objectMapper.writeValue(response.getWriter(), apiResponse);

}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.appsolve.wearther_backend.apiResponse.exception.handler;

import com.appsolve.wearther_backend.apiResponse.ApiResponse;
import com.appsolve.wearther_backend.apiResponse.exception.CustomException;
import com.appsolve.wearther_backend.apiResponse.exception.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtExceptionFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response); // 다음 필터로 요청 전달
} catch (ExpiredJwtException e) {
log.error("[JwtExceptionFilter] :: JWT 토큰이 만료되었습니다.");
handleException(response, ErrorCode.TOKEN_EXPIRED, "JWT 토큰이 만료되었습니다.");
} catch (JwtException e) {
log.error("[JwtExceptionFilter] :: 유효하지 않은 JWT 토큰입니다. {}", e.getMessage());
handleException(response, ErrorCode.INVALID_TOKEN, "유효하지 않은 JWT 토큰입니다.");
} catch (Exception e) {
log.error("[JwtExceptionFilter] :: 기타 예외 발생. {}", e.getMessage());
handleException(response, ErrorCode._INTERNAL_SERVER_ERROR, "서버 에러, 관리자에게 문의 바랍니다.");
}
}

private void handleException(HttpServletResponse response, ErrorCode errorCode, String message) throws IOException {
response.setStatus(errorCode.getHttpStatus().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

ApiResponse<Object> apiResponse = ApiResponse.fail(new CustomException(errorCode), null);
objectMapper.writeValue(response.getWriter(), apiResponse);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.appsolve.wearther_backend.config;

import com.appsolve.wearther_backend.apiResponse.exception.handler.CustomAccessDeniedHandler;
import com.appsolve.wearther_backend.apiResponse.exception.handler.CustomAuthenticationEntryPointHandler;
import com.appsolve.wearther_backend.apiResponse.exception.handler.JwtExceptionFilter;
import com.appsolve.wearther_backend.config.jwt.JwtAuthorizationFilter;
import com.appsolve.wearther_backend.config.jwt.JwtProvider;
import com.appsolve.wearther_backend.Repository.MemberRepository;
@@ -16,6 +19,7 @@
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.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
@@ -29,6 +33,11 @@
public class SecurityConfig{
private final MemberRepository memberRepository;
private final JwtProvider jwtProvider;
private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final JwtAuthorizationFilter jwtAuthorizationFilter;
private final JwtExceptionFilter jwtExceptionFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -38,10 +47,10 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration a
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain SercurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
public SecurityFilterChain SecurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**","/error").permitAll()
.requestMatchers("member/signUp", "member/login", "member/duplication-check").permitAll()
.requestMatchers("/images/**", "/js/**", "/webjars/**").permitAll()
.anyRequest().authenticated()
@@ -51,7 +60,13 @@ public SecurityFilterChain SercurityFilterChain(HttpSecurity http, Authenticatio
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안 함
.httpBasic(AbstractHttpConfigurer::disable) // HTTP Basic 인증 비활성화
.formLogin(AbstractHttpConfigurer::disable) // 폼 기반 로그인 비활성
.addFilterAfter(new JwtAuthorizationFilter(memberRepository,jwtProvider), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtExceptionFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthorizationFilter, JwtExceptionFilter.class)
.exceptionHandling(conf -> conf
.authenticationEntryPoint(customAuthenticationEntryPointHandler)
.accessDeniedHandler(customAccessDeniedHandler)
);

return http.build();
}
@Bean
Original file line number Diff line number Diff line change
@@ -7,21 +7,21 @@
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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@RequiredArgsConstructor
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;
private final MemberRepository memberRepository;

public JwtAuthorizationFilter(MemberRepository memberRepository, JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
this.memberRepository =memberRepository;
}


@Override
@@ -31,19 +31,20 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
Long userId = jwtProvider.getUserIdFromToken(token);
if (userId != null) {
MemberEntity member = memberRepository.findByMemberId(userId);
if (member!=null){
PrincipalDetails principalDetails = new PrincipalDetails(member);
Authentication authentication = new UsernamePasswordAuthenticationToken(
principalDetails,
null,
principalDetails.getAuthorities()

);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
SecurityContextHolder.getContext().setAuthentication(authentication);}
}
} else {
chain.doFilter(request, response);
}
}}
chain.doFilter(request, response);
}
}



Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
@@ -15,7 +16,7 @@
import java.util.HashMap;
import java.util.Map;


@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {
@@ -62,23 +63,26 @@ public String createToken(
}


public void validateToken(final String token) {
public boolean validateToken(final String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException e) {
throw new CustomException(ErrorCode.INVALID_SIGNATURE);
log.warn("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
throw new CustomException(ErrorCode.INVALID_TOKEN);
log.warn("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
throw new CustomException(ErrorCode.TOKEN_EXPIRED);
log.warn("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
throw new CustomException(ErrorCode.UNSUPPORTED_TOKEN);
log.warn("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
throw new CustomException(ErrorCode.EMPTY_CLAIMS);
log.warn("JWT claims string is empty: {}", e.getMessage());
}
return false;


}

@@ -105,16 +109,13 @@ public <T> T getUserClaimFromToken(final String token,String claim , Class<T> cl
}

public Claims getUserAllClaimFromToken(final String token) {
validateToken(token);
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims;
}


Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims;


}
}