Skip to content

Commit

Permalink
refactor : auths 단 refactoring (#56)
Browse files Browse the repository at this point in the history
1. JwtExceptionHandlerFilter.java 추가
- JwtAuthenticationFilter.java 와 역할 분리

2. AuthService.java 단 예외 처리 구체화
- JwtExceptionAdvice.java 에서 처리

3. JwtAuthenticationFilter.java - shouldNotFilter 조건 추가
- 로컬에서 h2 console 을 이용하여 테스트 시, accessToken null 예외가 발생해서 필터링 되지 않도록 처리

4. 로그 추가
  • Loading branch information
DreamChaserDeekay authored Dec 31, 2022
1 parent 958f4eb commit 5558bff
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package seb4141preproject.security.auth.advice;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import seb4141preproject.utils.ErrorResponse;

@Slf4j
@RestControllerAdvice
public class JwtExceptionAdvice {

@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleExpiredJwtException(ExpiredJwtException e) {
log.error("만료된 Refresh Token 입니다.", e);

return ErrorResponse.of(HttpStatus.UNAUTHORIZED, e.getMessage());
}

@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleMalformedJwtException(MalformedJwtException e) {
log.error("잘못된 Refresh Token 서명입니다.", e);

return ErrorResponse.of(HttpStatus.UNAUTHORIZED, e.getMessage());
}

@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleUnsupportedJwtException(UnsupportedJwtException e) {
log.error("지원되지 않는 Access Token 입니다.", e);

return ErrorResponse.of(HttpStatus.UNAUTHORIZED, e.getMessage());
}

@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
log.error("Refresh Token 이 잘못되었습니다.", e);

return ErrorResponse.of(HttpStatus.UNAUTHORIZED, e.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ public class CustomFilterConfiguration extends AbstractHttpConfigurer<CustomFilt

@Override
public void configure(HttpSecurity http) { // Custom Filter 추가 (jwtTokenizer 주입)
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtTokenizer, authService);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtTokenizer);
JwtExceptionHandlerFilter jwtExceptionHandlerFilter = new JwtExceptionHandlerFilter(jwtTokenizer, authService);

http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import seb4141preproject.security.auth.dto.*;
import seb4141preproject.security.auth.service.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.NoSuchElementException;

@RestController
@RequestMapping("/api/auths")
Expand All @@ -33,13 +33,12 @@ public ResponseEntity login(@RequestBody LoginDto loginDto, HttpServletResponse
// reissue 는 엔드포인트 따로 필요 없이 내부적으로 로직 처리

@PostMapping("/logout")
public ResponseEntity logout(HttpServletRequest request,
@AuthenticationPrincipal UserDetails user) {
public ResponseEntity logout(@AuthenticationPrincipal UserDetails user) {
if (user != null) {
authService.logout(user);
return new ResponseEntity<>("Logout Successful!", HttpStatus.OK);
} else {
return new ResponseEntity("User not Found", HttpStatus.NOT_FOUND);
throw new NoSuchElementException("회원 정보를 불러올 수 없습니다. 토큰을 확인 해주세요.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package seb4141preproject.security.auth.filter;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import seb4141preproject.security.auth.dto.TokenDto;
import seb4141preproject.security.auth.provider.*;
import seb4141preproject.security.auth.service.AuthService;
import seb4141preproject.security.auth.utils.HeaderMapRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
Expand All @@ -18,50 +16,38 @@
import java.util.Arrays;

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
private final AuthService authService;

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

String accessToken = resolveAccessToken(request); // request header 에서 accessToken 추출
HeaderMapRequestWrapper wrapper = new HeaderMapRequestWrapper(request); // (reissue 시) header 값 덮어쓰기를 위한 wrapper 객체 생성

int validateResult = jwtTokenizer.validateToken(accessToken);

if (validateResult == 0) { // validateToken 이상 없을 경우
log.info("Authentication Filter validateToken 실행");

if (jwtTokenizer.validateToken(accessToken)) {
Authentication authentication = jwtTokenizer.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);

} else if (validateResult == 1) { // jwt 토큰이 만료되었을 경우

String refreshToken = resolveRefreshToken(request); // request header 에서 refreshToken 추출

Authentication authentication = jwtTokenizer.getAuthentication(refreshToken); // refreshToken 으로 authentication 생성
// -> 만료된 accessToken 으로는 getAuthentication 불가능하기 때문
SecurityContextHolder.getContext().setAuthentication(authentication);

TokenDto tokenDto = authService.reissue(request, authentication); // 토큰 재발급

setHeaderResponse(wrapper, response, tokenDto); // header, response 값 재설정

}
// validateToken 결과값이 2(이상 없음 or 토큰 만료가 아닌 겅우)인 경우 바로 다음 필터링 진행
filterChain.doFilter(wrapper, response);
filterChain.doFilter(request, response);
}

@Override
// 해당 url 들은 filter 를 거치지 않도록 설정 (토큰 인증이 필요 없는 endpoint)
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI(); // URL
String method = request.getMethod(); // request 의 메소드 종류
String[] urls = new String[] {"/api/members", "/api/auths/login"}; // 회원가입, 로그인은 토큰이 필요 없음
String[] urls = new String[] {"/api/members", "/api/auths/login", "/h2.*"}; // 회원가입, 로그인, h2는 토큰이 필요 없음

return Arrays.stream(urls).anyMatch(s -> s.equals(path)) || method.equals("GET");
log.info("path : " + path);
log.info("match ? : " + Arrays.stream(urls).anyMatch(s -> path.matches(s)));

return Arrays.stream(urls).anyMatch(s -> path.matches(s))
|| (!path.matches(".*/votes/me") && method.equals("GET"));
}

private String resolveAccessToken(HttpServletRequest request) {
Expand All @@ -72,26 +58,4 @@ private String resolveAccessToken(HttpServletRequest request) {

return null;
}

private String resolveRefreshToken(HttpServletRequest request) {
String bearerToken = request.getHeader("RefreshToken");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}

return null;
}

private void setHeaderResponse(HeaderMapRequestWrapper wrapper,
HttpServletResponse response,
TokenDto tokenDto) {

// accessToken, refreshToken Header 값 새로 덮어쓰기
wrapper.addHeader("Authorization", tokenDto.getAccessToken());
wrapper.addHeader("RefreshToken", tokenDto.getRefreshToken());

// reissue 후 response header 에 새로운 accessToken, refreshToken 값 추가
response.addHeader("Authorization", tokenDto.getAccessToken());
response.addHeader("RefreshToken", tokenDto.getRefreshToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package seb4141preproject.security.auth.filter;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import seb4141preproject.security.auth.dto.TokenDto;
import seb4141preproject.security.auth.provider.JwtTokenizer;
import seb4141preproject.security.auth.service.AuthService;
import seb4141preproject.security.auth.utils.ErrorResponder;
import seb4141preproject.security.auth.utils.HeaderMapRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtExceptionHandlerFilter extends OncePerRequestFilter {

private final JwtTokenizer jwtTokenizer;
private final AuthService authService;

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

HeaderMapRequestWrapper wrapper = new HeaderMapRequestWrapper(request); // (reissue 시) header 값 덮어쓰기를 위한 wrapper 객체 생성

try {
filterChain.doFilter(request, response);

log.info("Exception Filter filterChain 실행");

} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.error("잘못된 Access Token 서명입니다.", e);
ErrorResponder.sendJwtErrorResponse(response, e.getMessage());

} catch (UnsupportedJwtException e) {
ErrorResponder.sendJwtErrorResponse(response, e.getMessage());
log.error("지원되지 않는 Access Token 입니다.", e);

} catch (IllegalArgumentException e) {
ErrorResponder.sendJwtErrorResponse(response, e.getMessage());
log.error("Access Token 이 잘못되었습니다.", e);

} catch (ExpiredJwtException e) {
log.error("만료된 Access Token 입니다. 재발급을 진행합니다.");

String refreshToken = resolveRefreshToken(request); // request header 에서 refreshToken 추출

Authentication authentication = jwtTokenizer.getAuthentication(refreshToken); // refreshToken 으로 authentication 생성
// -> 만료된 accessToken 으로는 getAuthentication 불가능하기 때문
SecurityContextHolder.getContext().setAuthentication(authentication);

TokenDto tokenDto = authService.reissue(request, authentication); // 토큰 재발급
setHeaderResponse(wrapper, response, tokenDto); // header, response 값 재설정

log.error("Access Token, Refresh Token 재발급이 완료되었습니다.");

filterChain.doFilter(wrapper, response);
}
}

private String resolveRefreshToken(HttpServletRequest request) {
String bearerToken = request.getHeader("RefreshToken");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}

return null;
}

private void setHeaderResponse(HeaderMapRequestWrapper wrapper,
HttpServletResponse response,
TokenDto tokenDto) {
// accessToken, refreshToken Header 값 새로 덮어쓰기
wrapper.addHeader("Authorization", tokenDto.getAccessToken());
wrapper.addHeader("RefreshToken", tokenDto.getRefreshToken());

// reissue 후 response header 에 새로운 accessToken, refreshToken 값 추가
response.addHeader("Authorization", tokenDto.getAccessToken());
response.addHeader("RefreshToken", tokenDto.getRefreshToken());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import seb4141preproject.security.auth.utils.*;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class AuthSuccessHandler implements AuthenticationSuccessHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,14 @@ public Authentication getAuthentication(String accessToken) {
}

// 토큰 정보를 검증
public int validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return 0;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.error("잘못된 JWT 서명입니다.", e);
} catch (ExpiredJwtException e) {
log.error("만료된 JWT 토큰입니다. 재발급을 진행합니다.", e);
return 1;
} catch (UnsupportedJwtException e) {
log.error("지원되지 않는 JWT 토큰입니다.", e);
} catch (IllegalArgumentException e) {
log.error("JWT 토큰이 잘못되었습니다.", e);
}
return 2;
public boolean validateToken(String token) {
Jwts
.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);

return true;
}

public Claims parseClaims(String accessToken) { // 토큰 정보 확인 (만료 토큰도 확인 가능)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ public TokenDto reissue(HttpServletRequest request, Authentication authenticatio
String refreshToken = request.getHeader("refreshToken").substring(7);

// 1. Refresh Token 검증
if (jwtTokenizer.validateToken(refreshToken) == 1) { // refreshToken 이 만료된 경우
// 강제 로그아웃 처리 필요
} else if (jwtTokenizer.validateToken(refreshToken) == 2) {
throw new RuntimeException("Refresh Token 이 유효하지 않습니다.");
}
jwtTokenizer.validateToken(refreshToken); // 서비스 단의 exception은 JwtExceptionAdvice에서 처리

// 2. 인증 정보를 기반으로 토큰 생성
TokenDto tokenDto = createToken(authentication);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@ public static void sendErrorResponse(HttpServletResponse response,
response.setStatus(status.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}

public static void sendJwtErrorResponse(HttpServletResponse response,
String message) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED, message);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}

0 comments on commit 5558bff

Please sign in to comment.