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

refactor : auths 단 refactoring #56

Merged
merged 1 commit into from
Dec 31, 2022
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
@@ -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));
}
}