JwtUtil
이번 과제에서는 JWT 토큰을 이용한 인증, 인가를 구현해야한다.
그렇기 때문에 JWT 토큰 생성만을 위한 Util 클래스를 만들었다.
@Component
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_KEY = "auth";
public static final String BEARER_PREFIX = "Bearer ";
private final long ACCESS_TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init(){
byte[] keyBytes = secretKey.getBytes();
key = Keys.hmacShaKeyFor(keyBytes);
}
public String createAccessToken(Long id, String role){
Date date = new Date();
Claims claims = Jwts.claims();
claims.put("id", id);
claims.put(AUTHORIZATION_KEY, role);
return BEARER_PREFIX+
Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME))
.setIssuedAt(date)
.signWith(key,signatureAlgorithm)
.compact();
}
public void addJwtToHeader(HttpServletResponse response, String token){
logger.info("토큰 추가");
try {
token = URLEncoder.encode(token, "UTF-8").replaceAll("\\+", "%20");
response.setHeader(AUTHORIZATION_HEADER,token);
} catch (UnsupportedEncodingException e) {
throw new CustomException(ErrorCode.INVALID_ENCODE);
}
}
public Claims getUserInfo(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
public String getTokenFromRequest(HttpServletRequest request) throws UnsupportedEncodingException {
String token = request.getHeader(AUTHORIZATION_HEADER);
token = URLDecoder.decode(token,"UTF-8");
if (!(StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX))) throw new CustomException(ErrorCode.NULL_TOKEN);
return token.substring(BEARER_PREFIX.length());
}
}
LogFilter
가장 먼저 지나치는 필터로 다음 필터에서 일어나는 예외들을 잡아서 던져준다.
@Slf4j
@Component
@Order(0)
public class LogFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
log.info("로그 필터 시작");
filterChain.doFilter(request, response);
}
catch (SecurityException | MalformedJwtException | SignatureException e) {
setErrorResponse(response, ErrorCode.INVALID_TOKEN);
} catch (ExpiredJwtException e) {
setErrorResponse(response, ErrorCode.NOT_VALID_TOKEN);
} catch (UnsupportedJwtException e) {
setErrorResponse(response, ErrorCode.UNSUPPORTED_TOKEN);
} catch (IllegalArgumentException | NullPointerException e) {
setErrorResponse(response,ErrorCode.NULL_TOKEN);
}
catch (CustomException e){
setErrorResponse(response,e.getErrorCode());
}
log.info("로그 필터 종료");
}
private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) {
response.setStatus(errorCode.getHttpStatus());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
try {
String json = new ObjectMapper().writeValueAsString(new ErrorMessageResponseDto(errorCode));
response.getWriter().write(json);
} catch(Exception e) {
response.getStatus();
}
}
}
TokenFilter
전반적인 Token 검증 및 유저 권한을 확인하는 필터이다.
@Slf4j
@Component
@Order(1)
@RequiredArgsConstructor
public class TokenFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository repository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("토큰 필터 시작");
String uri = request.getRequestURI();
// 로그인과 회원가입 요청은 토큰 인증처리 제외
if(uri.startsWith("/api/user/join") || uri.startsWith("/api/user/login")) filterChain.doFilter(request, response);
String token = jwtUtil.getTokenFromRequest(request);
// 들어온 요청이 일정 수정 및 삭제라면 Token에서 유저 권한정보를 꺼내어서
// 관리자 권한 유저인지 확인 후 없다면 예외처리 관리자 권한이 있는 유저라면
// 계속 진행
if(uri.startsWith("/api/todo/modify")|| uri.startsWith("/api/todo/delete")){
if(!jwtUtil.getUserInfo(token).get("auth").equals(UserRole.ADMIN.getAuthority()))throw new CustomException(ErrorCode.NOT_ADMIN);
}
Long id = jwtUtil.getUserInfo(token).get("id", Long.class);
User user = repository.findById(id).orElseThrow(()-> new CustomException(ErrorCode.NOT_USER_ID));
request.setAttribute("user", user);
filterChain.doFilter(request, response);
log.info("토큰 필터 종료");
}
}
트러블 슈팅
서로 다른 예외코드를 던져줘야하는데 같은 Exception를 일으키는 부분들이 있어서 이 부분을 어떻게 할지 고민하며
방법을 찾던 중 CustomException클래스를 따로 만들어서 관리를 할 수 있다는걸 알게되어서 CustomException 클래스를따로 만들고, 각각의 예외마다 알맞은 msg를 전해주기 위해 ErrorCode enum을 생성하여서 상태코드와 메세지를
각 상황에 맞게 보내줄 수 있도록 하였다. 지금까지는 그냥 Exception만 던져줬었는데 이번에 직접 ErrorCode enum과
커스텀 입섹션 클래스들을 만들어서 각각의 상황에 맞는 상태코드와 메세지를 던져주니 포스트맨으로 테스트하며
과제 요구사항들을 구현할 때 정말 도움이 많이 되었다. 앞으로도 예외처리는 이번에 했던 방법을 정말 잘 이용할 것 같다.
@Getter
public enum ErrorCode {
NOT_VALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효기간 만료된 토큰"),
UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED,"지원하지 않는 토큰"),
NOT_MATCH_LOGIN(HttpStatus.UNAUTHORIZED,"이메일과 비밀번호 틀림"),
EMAIL_DUPLICATION(HttpStatus.UNAUTHORIZED,"중복된 이메일입니다."),
NOT_MANAGER(HttpStatus.UNAUTHORIZED,"담당 유저로 등록되어 있지 않습니다."),
NULL_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 존재하지 않습니다."),
NO_API_DATA(HttpStatus.BAD_REQUEST,"api 데이터가 존재하지 않습니다."),
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 JWT 서명."),
INVALID_ENCODE(HttpStatus.BAD_REQUEST, "인코딩 에러"),
NOT_TODO_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 TODO ID"),
NO_MANAGER_MY(HttpStatus.BAD_REQUEST,"본인이 쓴 글에 본인을 담당 유저로 배치할 수 없습니다."),
NO_MY_WRITE_TODO(HttpStatus.BAD_REQUEST,"타인의 일정에 다른 유저를 담당유저로 배치할 수 없습니다."),
NOT_COMMENT_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 Comment ID"),
NOT_USER_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 USER ID"),
DIFFERENT_USER(HttpStatus.FORBIDDEN,"본인 계정만 삭제 및 수정할 수 있습니다."),
NO_MY_WRITE_COMMENT(HttpStatus.FORBIDDEN,"본인이 쓴 댓글만 삭제 및 수정 할 수 있습니다."),
MANAGER_DUPLICATION(HttpStatus.FORBIDDEN,"이미 담당 유저로 배치 되어 있는 일정입니다."),
NOT_ADMIN(HttpStatus.FORBIDDEN,"권한이 없습니다.");
private final int httpStatus;
private final String message;
//httpStatus = 현재 httpstatus 코드를 전달해준다.
//message = 해당 에러가 왜 일어났는지 알 수 있도록 도와주도록 메세지를 전달해주는 역할
ErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus.value();
this.message = message;
}
}
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
// 발생한 예외 상황에 대해 전달하기 위한 데이터
ErrorCode errorCode;
}
@ControllerAdvice
@Slf4j
public class CustomExceptionHandler {
// CustomException 발생 시 프런트로 보낼 ErrorMessageResponseDto를 생성 및 전달 해주는 클래스
@ExceptionHandler(value = CustomException.class)
public ResponseEntity<ErrorMessageResponseDto> CustomException(CustomException e) {
return returnResponse(e.getErrorCode());
}
private ResponseEntity<ErrorMessageResponseDto> returnResponse(ErrorCode errorCode) {
ErrorMessageResponseDto responseDto = new ErrorMessageResponseDto(errorCode);
return ResponseEntity.status(responseDto.getErrorCode()).body(responseDto);
}
}
@Getter
public class ErrorMessageResponseDto {
private int errorCode;
private String message;
// 데이터 전달 예시
// errCode : HttpStatus.FORBIDDEN
// message : "권한이 없습니다"
public ErrorMessageResponseDto(ErrorCode errorCode) {
this.errorCode = errorCode.getHttpStatus();
this.message = errorCode.getMessage();
}
}
회고
Jwt토큰과 필터를 이번에 처음 사용하다보니 강의에서 예제로 쓴 코드들을 분석하는 시간이 많았었던 것 같다.
처음에는 이게 어떻게 흘러가는 것인지 흐름조차 느낌이 오지 않았지만 계속 들여다보고 처음보는 메서드같은건 검색을하며 이래서 쓰는거구나 하고 직접 내 과제에도 적용해보면서 이제는 어느정도 흘러가는 느낌은 알게된 것 같다.
처음에는 TokenFilter 클래스의 doFilter 내부에 if문들이 많았었는데 리팩토링을 하다보니 쓸데없는 것들이 너무 많은것을 보며 코드를 다짜고 리팩토링은 꼭 해야겠구나 하는 생각이 들었다.
'TIL' 카테고리의 다른 글
JPA - 일정관리 앱 만들기 (1) | 2024.10.17 |
---|---|
JPA - 회원가입 로그인 (0) | 2024.10.17 |
영속성 컨텍스트 (1) | 2024.10.16 |
페이징 조회 - Pageble / PageRequest (0) | 2024.10.15 |
JPA - Entity (1) | 2024.10.15 |