[REST API] 12. Token 실습

문정준's avatar
May 09, 2025
[REST API] 12. Token 실습
  • FilterConfig
    • 인증에 사용할 AuthorizationFilter를 Filter에 삽입
@Bean public FilterRegistrationBean<AuthorizationFilter> authorizationFilter() { FilterRegistrationBean<AuthorizationFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new AuthorizationFilter()); registrationBean.addUrlPatterns("/s/*"); // 모든 요청에 적용 registrationBean.setOrder(2); // 필터 순서 설정 return registrationBean; }
 
  • AuthorizationFilter
    • 요청의 Header에서 ‘Authorization’의 Token을 확인
    • 토큰이 없거나, 만료기간이 지났거나, 프로토콜을 준수하지 않았거나, 값이 일치하지 않을 경우 오류 발생
    • Status : 401 (Unauthorized)
package shop.mtcoding.blog._core.filter; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import shop.mtcoding.blog._core.util.JwtUtil; import shop.mtcoding.blog._core.util.Resp; import shop.mtcoding.blog.user.User; import java.io.IOException; import java.io.PrintWriter; public class AuthorizationFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String accessToken = request.getHeader("Authorization"); try { if (accessToken == null || accessToken.isBlank()) throw new RuntimeException("토큰을 전달해주세요"); if (!accessToken.startsWith("Bearer ")) throw new RuntimeException("Bearer 프로토콜 지켜야지 짜식아"); accessToken = accessToken.replace("Bearer ", ""); User user = JwtUtil.verify(accessToken); // 토큰 재검증 회피를 위한 임시 저장용 session HttpSession session = request.getSession(); session.setAttribute("sessionUser", user); chain.doFilter(request, response); } catch (TokenExpiredException e1) { e1.printStackTrace(); exResponse(response, "토큰이 만료되었습니다"); } catch (JWTDecodeException | SignatureVerificationException e2) { e2.printStackTrace(); exResponse(response, "토큰 검증에 실패했어요"); } catch (RuntimeException e3) { e3.printStackTrace(); exResponse(response, e3.getMessage()); } } private void exResponse(HttpServletResponse response, String msg) throws IOException { response.setContentType("application/json;charset=utf-8"); response.setStatus(401); PrintWriter out = response.getWriter(); Resp<?> resp = Resp.fail(401, msg); String responseBody = new ObjectMapper().writeValueAsString(resp); out.println(responseBody); } }
 
  • GlobalExceptionHandler
    • REST API → 기존 Exception을 전부 삭제하고, 데이터를 전송하는 ExceptionApi만 남김
    • 실제 상태 코드도 바꿔서 돌려줘야 하므로, ResponseEntity 사용
      • body, status code를 받음
package shop.mtcoding.blog._core.error; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import shop.mtcoding.blog._core.error.ex.ExceptionApi400; import shop.mtcoding.blog._core.error.ex.ExceptionApi401; import shop.mtcoding.blog._core.error.ex.ExceptionApi403; import shop.mtcoding.blog._core.error.ex.ExceptionApi404; import shop.mtcoding.blog._core.util.Resp; @RestControllerAdvice // @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ExceptionApi400.class) public ResponseEntity<?> exApi400(ExceptionApi400 e) { Resp<?> resp = Resp.fail(400, e.getMessage()); return new ResponseEntity<>(resp, HttpStatus.BAD_REQUEST); } @ExceptionHandler(ExceptionApi401.class) public ResponseEntity<?> exApi401(ExceptionApi401 e) { Resp<?> resp = Resp.fail(401, e.getMessage()); return new ResponseEntity<>(resp, HttpStatus.UNAUTHORIZED); } @ExceptionHandler(ExceptionApi403.class) public ResponseEntity<?> exApi403(ExceptionApi403 e) { Resp<?> resp = Resp.fail(403, e.getMessage()); return new ResponseEntity<>(resp, HttpStatus.FORBIDDEN); } @ExceptionHandler(ExceptionApi404.class) public ResponseEntity<?> exApi404(ExceptionApi404 e) { Resp<?> resp = Resp.fail(404, e.getMessage()); return new ResponseEntity<>(resp, HttpStatus.NOT_FOUND); } @ExceptionHandler(Exception.class) public ResponseEntity<?> exUnKnown(Exception e) { System.out.println("관리자님 보세요 : " + e.getMessage()); // 로그를 파일에 기록해서 나중에 봐야함 Resp<?> resp = Resp.fail(500, "관리자에게 문의하세요"); return new ResponseEntity<>(resp, HttpStatus.INTERNAL_SERVER_ERROR); } }
 
  • JwtUtil
    • JWT 생성 함수 : create(User)
      • withSubject : 토큰 사용처 (제목)
      • withExpiresAt : 유효 시간 (LocalDate 타입)
      • withClaim : 포함할 내용 (Payload, 여러 개 추가 가능)
      • sign : 암호화 기법을 고르고, 암호화에 사용할 Secret Key 작성
    • JWT 인증 함수 : verify(JWT)
      • JWT.require : Secret Key를 포함한 암호화 기법을 사용하여 전달 받은 JWT를 복호화
      • 복호화한 JWT에 담겨있는 정보만을 가지고 Fake User를 만들어 넘겨줘야 함
        • Token은 개인 정보를 담아선 안됨 (누구나 열람이 가능)
package shop.mtcoding.blog._core.util; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import shop.mtcoding.blog.user.User; import java.util.Date; public class JwtUtil { public static String create(User user) { String jwt = JWT.create() .withSubject("blog") .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) .withClaim("id", user.getId()) .withClaim("username", user.getUsername()) .sign(Algorithm.HMAC512("metacoding")); return jwt; } public static User verify(String jwt) { DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512("metacoding")).build().verify(jwt); int id = decodedJWT.getClaim("id").asInt(); String username = decodedJWT.getClaim("username").asString(); return User.builder() .id(id) .username(username) .build(); } }
 
  • UserService
public UserResponse.TokenDTO 로그인(UserRequest.LoginDTO loginDTO) { User userPS = userRepository.findByUsername(loginDTO.getUsername()) .orElseThrow(() -> new ExceptionApi401("유저네임 혹은 비밀번호가 틀렸습니다")); boolean isSame = BCrypt.checkpw(loginDTO.getPassword(), userPS.getPassword()); if (!isSame) throw new ExceptionApi401("유저네임 혹은 비밀번호가 틀렸습니다."); // 토큰 생성 String accessToken = JwtUtil.create(userPS); return UserResponse.TokenDTO.builder().accessToken(accessToken).build(); }
 
  • 각 Service
    • 인증이 필요한 경우의 인터셉터 교체 (인증은 필터에서 진행)

결과

  • 로그인을 하여 새 토큰 발행 후 댓글 삭제
notion image
 

성공

notion image
 

실패

  1. 토큰이 없을 때
notion image
 
  1. 토큰 프로토콜이 올바르지 않을 때
notion image
 
Share article

sxias