Skip to content

Commit

Permalink
[#43] feat: Exception handling 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
jinyoungchoi95 committed Sep 8, 2021
1 parent 4ee881d commit 65176b2
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import static org.springframework.http.HttpMethod.POST;

import com.study.realworld.security.JwtAuthenticationEntiyPoint;
import com.study.realworld.security.JwtAuthenticationTokenFilter;
import com.study.realworld.security.JwtExceptionFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -17,9 +19,15 @@
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final JwtExceptionFilter jwtExceptionFilter;
private final JwtAuthenticationEntiyPoint jwtAuthenticationEntiyPoint;

public WebSecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
public WebSecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter,
JwtExceptionFilter jwtExceptionFilter,
JwtAuthenticationEntiyPoint jwtAuthenticationEntiyPoint) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.jwtExceptionFilter = jwtExceptionFilter;
this.jwtAuthenticationEntiyPoint = jwtAuthenticationEntiyPoint;
}

@Bean
Expand All @@ -43,6 +51,7 @@ protected void configure(HttpSecurity http) throws Exception {
.disable();
http
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntiyPoint)
.and()

.sessionManagement()
Expand All @@ -57,8 +66,9 @@ protected void configure(HttpSecurity http) throws Exception {
.formLogin()
.disable();
http
.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationTokenFilter.class)
;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

public enum ErrorCode {

// user
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "user is not found"),

EMAIL_DUPLICATION(HttpStatus.CONFLICT, "Duplicated email exists."),
USERNAME_DUPLICATION(HttpStatus.CONFLICT, "Duplicated username exists."),

PASSWORD_DISMATCH(HttpStatus.FORBIDDEN, "password is dismatch."),

INVALID_USERNAME_NULL(HttpStatus.BAD_REQUEST, "username must be provided."),
Expand All @@ -16,7 +18,13 @@ public enum ErrorCode {
INVALID_EMAIL_NULL(HttpStatus.BAD_REQUEST, "address must be provided."),
INVALID_EMAIL_PATTERN(HttpStatus.BAD_REQUEST, "address must be provided by limited pattern like '[email protected]'."),
INVALID_PASSWORD_NULL(HttpStatus.BAD_REQUEST, "password must be provided."),
INVALID_PASSWORD_LENGTH(HttpStatus.BAD_REQUEST, "password length must be between 6 and 20 characters.")
INVALID_PASSWORD_LENGTH(HttpStatus.BAD_REQUEST, "password length must be between 6 and 20 characters."),

// authentication
INVALID_EXPIRED_JWT(HttpStatus.BAD_REQUEST, "this jwt has expired."),
INVALID_MALFORMED_JWT(HttpStatus.BAD_REQUEST, "this jwt was malformed."),
INVALID_UNSUPPORTED_JWT(HttpStatus.BAD_REQUEST, "this jwt wat not supported."),
INVALID_ILLEGAL_ARGUMENT_JWT(HttpStatus.BAD_REQUEST, "this jwt was wrong.");
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.study.realworld.global.exception;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

@JsonTypeName("errors")
@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME)
public class ErrorResponse {

@JsonProperty("body")
private List<String> body;

protected ErrorResponse() {
}

private ErrorResponse(List<String> body) {
this.body = body;
}

public static String toJson(RuntimeException e) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(new ErrorResponse(List.of(e.getMessage())));
}

public static ResponseEntity<ErrorResponse> of(ErrorCode e) {
return ResponseEntity.status(e.getHttpStatus()).body(new ErrorResponse(List.of(e.getMessage())));
}

public static ResponseEntity<ErrorResponse> from(Exception e, HttpStatus httpStatus) {
return ResponseEntity.status(httpStatus).body(new ErrorResponse(List.of(e.getMessage())));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.study.realworld.global.exception;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException excpetion) {
log.debug("Bad request exception occurred : {}", excpetion.getMessage(), excpetion);
return ErrorResponse.from(excpetion, BAD_REQUEST);
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<?> handleMethodNotAllowedException(Exception exception) {
return ErrorResponse.from(exception, METHOD_NOT_ALLOWED);
}

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException exception) {
log.warn("Unexpected service exception occurred: {}", exception.getMessage(), exception);
return ErrorResponse.of(exception.getErrorCode());
}

// TODO
// @ExceptionHandler(Exception.class)
// public ResponseEntity<ErrorResponse> handleGlobalException(Exception exception) {
// log.error("Unexpected exception occurred: {}", exception.getMessage(), exception);
// return ErrorResponse.from(exception, BAD_REQUEST);
// }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.study.realworld.global.exception;

import org.springframework.http.HttpStatus;

public class JwtException extends RuntimeException {

private ErrorCode errorCode;

public JwtException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}

public HttpStatus getHttpStatus() {
return errorCode.getHttpStatus();
}

public int getHttpStatusValue() {
return getHttpStatus().value();
}

public String getMessage() {
return errorCode.getMessage();
}

}
14 changes: 10 additions & 4 deletions src/main/java/com/study/realworld/security/JjwtService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.study.realworld.security;

import com.study.realworld.global.exception.ErrorCode;
import com.study.realworld.global.exception.JwtException;
import com.study.realworld.user.domain.User;
import com.study.realworld.user.domain.UserRepository;
import io.jsonwebtoken.Claims;
Expand All @@ -13,12 +15,16 @@
import java.security.Key;
import java.util.Date;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JjwtService implements JwtService {

private static final Logger log = LoggerFactory.getLogger(JjwtService.class);

private final static String JWT_HEADER_PARAM_TYPE = "typ";

private final Key key;
Expand Down Expand Up @@ -62,13 +68,13 @@ public Optional<User> getUser(String accessToken) {
Long userId = Long.parseLong(claims.getSubject());
return userRepository.findById(userId);
} catch (ExpiredJwtException e) {
throw new RuntimeException("만료된 JWT 서명입니다.");
throw new JwtException(ErrorCode.INVALID_EXPIRED_JWT);
} catch (SecurityException | MalformedJwtException e) {
throw new MalformedJwtException("잘못된 JWT 서명입니다.");
throw new JwtException(ErrorCode.INVALID_MALFORMED_JWT);
} catch (UnsupportedJwtException e) {
throw new UnsupportedJwtException("지원되지 않는 JWT 서명입니다.");
throw new JwtException(ErrorCode.INVALID_UNSUPPORTED_JWT);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("JWT 토큰이 잘못되었습니다.");
throw new JwtException(ErrorCode.INVALID_ILLEGAL_ARGUMENT_JWT);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.study.realworld.security;

import com.study.realworld.global.exception.ErrorResponse;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class JwtAuthenticationEntiyPoint implements AuthenticationEntryPoint {

private static final String CONTENT_TYPE = "application/json";

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType(CONTENT_TYPE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

response.getWriter().write(ErrorResponse.toJson(authException));
}

}
43 changes: 43 additions & 0 deletions src/main/java/com/study/realworld/security/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.study.realworld.security;

import com.study.realworld.global.exception.ErrorResponse;
import com.study.realworld.global.exception.JwtException;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

private static final Logger log = LoggerFactory.getLogger(JwtExceptionFilter.class);

private static final String CONTENT_TYPE = "application/json";

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

try {
filterChain.doFilter(request, response);
} catch (JwtException exception) {
log.debug("Wrong jwt request exception occurred : {}", exception.getMessage());
sendErrorMessage(response, exception);
} catch (RuntimeException exception) {
log.error("Unexpected runtime exception occurred: {}", exception.getMessage(), exception);
}
}

private void sendErrorMessage(HttpServletResponse response, JwtException e) throws IOException {
response.setContentType(CONTENT_TYPE);
response.setStatus(e.getHttpStatusValue());

response.getWriter().write(ErrorResponse.toJson(e));
}

}
24 changes: 12 additions & 12 deletions src/test/java/com/study/realworld/security/JjwtServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.when;

import com.study.realworld.global.exception.ErrorCode;
import com.study.realworld.global.exception.JwtException;
import com.study.realworld.user.domain.User;
import com.study.realworld.user.domain.UserRepository;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
Expand Down Expand Up @@ -101,9 +101,9 @@ void getUserByExpiredTokenTest() {
.compact();

// when & then
assertThatExceptionOfType(RuntimeException.class)
assertThatExceptionOfType(JwtException.class)
.isThrownBy(() -> jwtTokenProvider.getUser(accessToken))
.withMessageMatching("만료된 JWT 서명입니다.");
.withMessageMatching(ErrorCode.INVALID_EXPIRED_JWT.getMessage());
}

@Test
Expand All @@ -114,9 +114,9 @@ void testValidateTokenByMalFormedToken() {
String accessToken = "te.st.";

// when & then
assertThatExceptionOfType(MalformedJwtException.class)
assertThatExceptionOfType(JwtException.class)
.isThrownBy(() -> jwtTokenProvider.getUser(accessToken))
.withMessageMatching("잘못된 JWT 서명입니다.");
.withMessageMatching(ErrorCode.INVALID_MALFORMED_JWT.getMessage());
}

@Test
Expand All @@ -130,9 +130,9 @@ void testValidateTokenByExpiredToken() {
.compact();

// when & then
assertThatExceptionOfType(RuntimeException.class)
assertThatExceptionOfType(JwtException.class)
.isThrownBy(() -> jwtTokenProvider.getUser(accessToken))
.withMessageMatching("만료된 JWT 서명입니다.");
.withMessageMatching(ErrorCode.INVALID_EXPIRED_JWT.getMessage());
}

@Test
Expand All @@ -147,9 +147,9 @@ void testValidateTokenByUnsupportedToken() {
.compact();

// when & then
assertThatExceptionOfType(UnsupportedJwtException.class)
assertThatExceptionOfType(JwtException.class)
.isThrownBy(() -> jwtTokenProvider.getUser(accessToken))
.withMessageMatching("지원되지 않는 JWT 서명입니다.");
.withMessageMatching(ErrorCode.INVALID_UNSUPPORTED_JWT.getMessage());
}

@Test
Expand All @@ -160,9 +160,9 @@ void testValidateTokenByIllegalArgumentToken() {
String accessToken = "";

// when & then
assertThatExceptionOfType(IllegalArgumentException.class)
assertThatExceptionOfType(JwtException.class)
.isThrownBy(() -> jwtTokenProvider.getUser(accessToken))
.withMessageMatching("JWT 토큰이 잘못되었습니다.");
.withMessageMatching(ErrorCode.INVALID_ILLEGAL_ARGUMENT_JWT.getMessage());
}

}
Loading

0 comments on commit 65176b2

Please sign in to comment.